aboutsummaryrefslogtreecommitdiffstats
path: root/org_apg/src/org/apg/ui
diff options
context:
space:
mode:
Diffstat (limited to 'org_apg/src/org/apg/ui')
-rw-r--r--org_apg/src/org/apg/ui/AboutActivity.java51
-rw-r--r--org_apg/src/org/apg/ui/BaseActivity.java436
-rw-r--r--org_apg/src/org/apg/ui/DecryptActivity.java807
-rw-r--r--org_apg/src/org/apg/ui/EditKeyActivity.java292
-rw-r--r--org_apg/src/org/apg/ui/EncryptActivity.java998
-rw-r--r--org_apg/src/org/apg/ui/GeneralActivity.java177
-rw-r--r--org_apg/src/org/apg/ui/ImportFromQRCodeActivity.java138
-rw-r--r--org_apg/src/org/apg/ui/KeyListActivity.java768
-rw-r--r--org_apg/src/org/apg/ui/KeyServerPreferenceActivity.java125
-rw-r--r--org_apg/src/org/apg/ui/KeyServerQueryActivity.java297
-rw-r--r--org_apg/src/org/apg/ui/MailListActivity.java222
-rw-r--r--org_apg/src/org/apg/ui/MainActivity.java419
-rw-r--r--org_apg/src/org/apg/ui/PreferencesActivity.java274
-rw-r--r--org_apg/src/org/apg/ui/PublicKeyListActivity.java191
-rw-r--r--org_apg/src/org/apg/ui/SecretKeyListActivity.java203
-rw-r--r--org_apg/src/org/apg/ui/SelectPublicKeyListActivity.java172
-rw-r--r--org_apg/src/org/apg/ui/SelectPublicKeyListAdapter.java226
-rw-r--r--org_apg/src/org/apg/ui/SelectSecretKeyListActivity.java115
-rw-r--r--org_apg/src/org/apg/ui/SelectSecretKeyListAdapter.java176
-rw-r--r--org_apg/src/org/apg/ui/SendKeyActivity.java100
-rw-r--r--org_apg/src/org/apg/ui/SignKeyActivity.java294
-rw-r--r--org_apg/src/org/apg/ui/widget/Editor.java25
-rw-r--r--org_apg/src/org/apg/ui/widget/IntegerListPreference.java95
-rw-r--r--org_apg/src/org/apg/ui/widget/KeyEditor.java233
-rw-r--r--org_apg/src/org/apg/ui/widget/KeyServerEditor.java78
-rw-r--r--org_apg/src/org/apg/ui/widget/SectionView.java335
-rw-r--r--org_apg/src/org/apg/ui/widget/UserIdEditor.java192
27 files changed, 7439 insertions, 0 deletions
diff --git a/org_apg/src/org/apg/ui/AboutActivity.java b/org_apg/src/org/apg/ui/AboutActivity.java
new file mode 100644
index 000000000..308a1e06e
--- /dev/null
+++ b/org_apg/src/org/apg/ui/AboutActivity.java
@@ -0,0 +1,51 @@
+package org.apg.ui;
+
+import org.apg.Constants;
+import org.apg.R;
+
+import android.app.Activity;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.TextView;
+
+public class AboutActivity extends Activity {
+ Activity mActivity;
+
+ /**
+ * Instantiate View for this Activity
+ */
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.about_activity);
+
+ mActivity = this;
+
+ TextView versionText = (TextView) findViewById(R.id.about_version);
+ versionText.setText(getString(R.string.about_version) + " " + getVersion());
+ }
+
+ /**
+ * Get the current package version.
+ *
+ * @return The current version.
+ */
+ private String getVersion() {
+ String result = "";
+ try {
+ PackageManager manager = mActivity.getPackageManager();
+ PackageInfo info = manager.getPackageInfo(mActivity.getPackageName(), 0);
+
+ result = String.format("%s (%s)", info.versionName, info.versionCode);
+ } catch (NameNotFoundException e) {
+ Log.w(Constants.TAG, "Unable to get application version: " + e.getMessage());
+ result = "Unable to get application version.";
+ }
+
+ return result;
+ }
+}
diff --git a/org_apg/src/org/apg/ui/BaseActivity.java b/org_apg/src/org/apg/ui/BaseActivity.java
new file mode 100644
index 000000000..9b5039a5d
--- /dev/null
+++ b/org_apg/src/org/apg/ui/BaseActivity.java
@@ -0,0 +1,436 @@
+/*
+ * 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.apg.ui;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Locale;
+
+import org.apg.Apg;
+import org.apg.AskForSecretKeyPassPhrase;
+import org.apg.Constants;
+import org.apg.Id;
+import org.apg.PausableThread;
+import org.apg.Preferences;
+import org.apg.ProgressDialogUpdater;
+import org.apg.Service;
+import org.apg.R;
+
+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.content.res.Configuration;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class BaseActivity extends Activity implements Runnable, ProgressDialogUpdater,
+ AskForSecretKeyPassPhrase.PassPhraseCallbackInterface {
+
+ private ProgressDialog mProgressDialog = null;
+ private PausableThread mRunningThread = null;
+ private Thread mDeletingThread = null;
+
+ private long mSecretKeyId = 0;
+ private String mDeleteFile = null;
+
+ protected Preferences mPreferences;
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ handlerCallback(msg);
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mPreferences = Preferences.getPreferences(this);
+ setLanguage(this, mPreferences.getLanguage());
+
+ Apg.initialize(this);
+
+ if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ File dir = new File(Constants.path.APP_DIR);
+ if (!dir.exists() && !dir.mkdirs()) {
+ // ignore this for now, it's not crucial
+ // that the directory doesn't exist at this point
+ }
+ }
+
+ startCacheService(this, mPreferences);
+ }
+
+ public static void startCacheService(Activity activity, Preferences preferences) {
+ Intent intent = new Intent(activity, Service.class);
+ intent.putExtra(Service.EXTRA_TTL, preferences.getPassPhraseCacheTtl());
+ activity.startService(intent);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, Id.menu.option.preferences, 0, R.string.menu_preferences).setIcon(
+ android.R.drawable.ic_menu_preferences);
+ menu.add(0, Id.menu.option.about, 1, R.string.menu_about).setIcon(
+ android.R.drawable.ic_menu_info_details);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case Id.menu.option.about: {
+ startActivity(new Intent(this, AboutActivity.class));
+ return true;
+ }
+
+ case Id.menu.option.preferences: {
+ startActivity(new Intent(this, PreferencesActivity.class));
+ return true;
+ }
+
+ case Id.menu.option.search: {
+ startSearch("", false, null, false);
+ return true;
+ }
+
+ default: {
+ break;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ // in case it is a progress dialog
+ mProgressDialog = new ProgressDialog(this);
+ mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ mProgressDialog.setCancelable(false);
+ switch (id) {
+ case Id.dialog.encrypting: {
+ mProgressDialog.setMessage(this.getString(R.string.progress_initializing));
+ return mProgressDialog;
+ }
+
+ case Id.dialog.decrypting: {
+ mProgressDialog.setMessage(this.getString(R.string.progress_initializing));
+ return mProgressDialog;
+ }
+
+ case Id.dialog.saving: {
+ mProgressDialog.setMessage(this.getString(R.string.progress_saving));
+ return mProgressDialog;
+ }
+
+ case Id.dialog.importing: {
+ mProgressDialog.setMessage(this.getString(R.string.progress_importing));
+ return mProgressDialog;
+ }
+
+ case Id.dialog.exporting: {
+ mProgressDialog.setMessage(this.getString(R.string.progress_exporting));
+ return mProgressDialog;
+ }
+
+ case Id.dialog.deleting: {
+ mProgressDialog.setMessage(this.getString(R.string.progress_initializing));
+ return mProgressDialog;
+ }
+
+ case Id.dialog.querying: {
+ mProgressDialog.setMessage(this.getString(R.string.progress_querying));
+ mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
+ mProgressDialog.setCancelable(false);
+ return mProgressDialog;
+ }
+
+ case Id.dialog.signing: {
+ mProgressDialog.setMessage(this.getString(R.string.progress_signing));
+ mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
+ mProgressDialog.setCancelable(false);
+ return mProgressDialog;
+ }
+
+ default: {
+ break;
+ }
+ }
+ mProgressDialog = null;
+
+ switch (id) {
+
+ case Id.dialog.pass_phrase: {
+ return AskForSecretKeyPassPhrase.createDialog(this, getSecretKeyId(), this);
+ }
+
+ case Id.dialog.pass_phrases_do_not_match: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ alert.setIcon(android.R.drawable.ic_dialog_alert);
+ alert.setTitle(R.string.error);
+ alert.setMessage(R.string.passPhrasesDoNotMatch);
+
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(Id.dialog.pass_phrases_do_not_match);
+ }
+ });
+ alert.setCancelable(false);
+
+ return alert.create();
+ }
+
+ case Id.dialog.no_pass_phrase: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ alert.setIcon(android.R.drawable.ic_dialog_alert);
+ alert.setTitle(R.string.error);
+ alert.setMessage(R.string.passPhraseMustNotBeEmpty);
+
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(Id.dialog.no_pass_phrase);
+ }
+ });
+ alert.setCancelable(false);
+
+ return alert.create();
+ }
+
+ case Id.dialog.delete_file: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ alert.setIcon(android.R.drawable.ic_dialog_alert);
+ alert.setTitle(R.string.warning);
+ alert.setMessage(this.getString(R.string.fileDeleteConfirmation, getDeleteFile()));
+
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(Id.dialog.delete_file);
+ final File file = new File(getDeleteFile());
+ showDialog(Id.dialog.deleting);
+ mDeletingThread = new Thread(new Runnable() {
+ public void run() {
+ Bundle data = new Bundle();
+ data.putInt(Constants.extras.STATUS, Id.message.delete_done);
+ try {
+ Apg.deleteFileSecurely(BaseActivity.this, file, BaseActivity.this);
+ } catch (FileNotFoundException e) {
+ data.putString(Apg.EXTRA_ERROR, BaseActivity.this.getString(
+ R.string.error_fileNotFound, file));
+ } catch (IOException e) {
+ data.putString(Apg.EXTRA_ERROR, BaseActivity.this.getString(
+ R.string.error_fileDeleteFailed, file));
+ }
+ Message msg = new Message();
+ msg.setData(data);
+ sendMessage(msg);
+ }
+ });
+ mDeletingThread.start();
+ }
+ });
+ alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(Id.dialog.delete_file);
+ }
+ });
+ alert.setCancelable(true);
+
+ return alert.create();
+ }
+
+ default: {
+ break;
+ }
+ }
+
+ return super.onCreateDialog(id);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.request.secret_keys: {
+ if (resultCode == RESULT_OK) {
+ Bundle bundle = data.getExtras();
+ setSecretKeyId(bundle.getLong(Apg.EXTRA_KEY_ID));
+ } else {
+ setSecretKeyId(Id.key.none);
+ }
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ public void setProgress(int resourceId, int progress, int max) {
+ setProgress(getString(resourceId), progress, max);
+ }
+
+ public void setProgress(int progress, int max) {
+ Message msg = new Message();
+ Bundle data = new Bundle();
+ data.putInt(Constants.extras.STATUS, Id.message.progress_update);
+ data.putInt(Constants.extras.PROGRESS, progress);
+ data.putInt(Constants.extras.PROGRESS_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(Constants.extras.STATUS, Id.message.progress_update);
+ data.putString(Constants.extras.MESSAGE, message);
+ data.putInt(Constants.extras.PROGRESS, progress);
+ data.putInt(Constants.extras.PROGRESS_MAX, max);
+ msg.setData(data);
+ mHandler.sendMessage(msg);
+ }
+
+ public void handlerCallback(Message msg) {
+ Bundle data = msg.getData();
+ if (data == null) {
+ return;
+ }
+
+ int type = data.getInt(Constants.extras.STATUS);
+ switch (type) {
+ case Id.message.progress_update: {
+ String message = data.getString(Constants.extras.MESSAGE);
+ if (mProgressDialog != null) {
+ if (message != null) {
+ mProgressDialog.setMessage(message);
+ }
+ mProgressDialog.setMax(data.getInt(Constants.extras.PROGRESS_MAX));
+ mProgressDialog.setProgress(data.getInt(Constants.extras.PROGRESS));
+ }
+ break;
+ }
+
+ case Id.message.delete_done: {
+ mProgressDialog = null;
+ deleteDoneCallback(msg);
+ break;
+ }
+
+ case Id.message.import_done: // intentionally no break
+ case Id.message.export_done: // intentionally no break
+ case Id.message.query_done: // intentionally no break
+ case Id.message.done: {
+ mProgressDialog = null;
+ doneCallback(msg);
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ }
+
+ public void doneCallback(Message msg) {
+
+ }
+
+ public void deleteDoneCallback(Message msg) {
+ removeDialog(Id.dialog.deleting);
+ mDeletingThread = null;
+
+ Bundle data = msg.getData();
+ String error = data.getString(Apg.EXTRA_ERROR);
+ String message;
+ if (error != null) {
+ message = getString(R.string.errorMessage, error);
+ } else {
+ message = getString(R.string.fileDeleteSuccessful);
+ }
+
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
+ }
+
+ public void passPhraseCallback(long keyId, String passPhrase) {
+ Apg.setCachedPassPhrase(keyId, passPhrase);
+ }
+
+ public void sendMessage(Message msg) {
+ mHandler.sendMessage(msg);
+ }
+
+ public PausableThread getRunningThread() {
+ return mRunningThread;
+ }
+
+ public void startThread() {
+ mRunningThread = new PausableThread(this);
+ mRunningThread.start();
+ }
+
+ public void run() {
+
+ }
+
+ public void setSecretKeyId(long id) {
+ mSecretKeyId = id;
+ }
+
+ public long getSecretKeyId() {
+ return mSecretKeyId;
+ }
+
+ protected void setDeleteFile(String deleteFile) {
+ mDeleteFile = deleteFile;
+ }
+
+ protected String getDeleteFile() {
+ return mDeleteFile;
+ }
+
+ public static void setLanguage(Context context, String language) {
+ Locale locale;
+ if (language == null || language.equals("")) {
+ locale = Locale.getDefault();
+ } else {
+ locale = new Locale(language);
+ }
+ Configuration config = new Configuration();
+ config.locale = locale;
+ context.getResources().updateConfiguration(config,
+ context.getResources().getDisplayMetrics());
+ }
+}
diff --git a/org_apg/src/org/apg/ui/DecryptActivity.java b/org_apg/src/org/apg/ui/DecryptActivity.java
new file mode 100644
index 000000000..cb58dfb09
--- /dev/null
+++ b/org_apg/src/org/apg/ui/DecryptActivity.java
@@ -0,0 +1,807 @@
+/*
+ * 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.apg.ui;
+
+import org.apg.Apg;
+import org.apg.Constants;
+import org.apg.DataDestination;
+import org.apg.DataSource;
+import org.apg.FileDialog;
+import org.apg.Id;
+import org.apg.InputData;
+import org.apg.PausableThread;
+import org.apg.provider.DataProvider;
+import org.apg.util.Compatibility;
+import org.spongycastle.jce.provider.BouncyCastleProvider;
+import org.spongycastle.openpgp.PGPException;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.apg.R;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.ActivityNotFoundException;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Message;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.animation.AnimationUtils;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.ViewFlipper;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.Security;
+import java.security.SignatureException;
+import java.util.regex.Matcher;
+
+public class DecryptActivity extends BaseActivity {
+ private long mSignatureKeyId = 0;
+
+ private Intent mIntent;
+
+ private boolean mReturnResult = false;
+ private String mReplyTo = null;
+ private String mSubject = null;
+ private boolean mSignedOnly = false;
+ private boolean mAssumeSymmetricEncryption = false;
+
+ private EditText mMessage = null;
+ private LinearLayout mSignatureLayout = null;
+ private ImageView mSignatureStatusImage = null;
+ private TextView mUserId = null;
+ private TextView mUserIdRest = null;
+
+ private ViewFlipper mSource = null;
+ private TextView mSourceLabel = null;
+ private ImageView mSourcePrevious = null;
+ private ImageView mSourceNext = null;
+
+ private Button mDecryptButton = null;
+ private Button mReplyButton = null;
+
+ private int mDecryptTarget;
+
+ private EditText mFilename = null;
+ private CheckBox mDeleteAfter = null;
+ private ImageButton mBrowse = null;
+
+ private String mInputFilename = null;
+ private String mOutputFilename = null;
+
+ private Uri mContentUri = null;
+ private byte[] mData = null;
+ private boolean mReturnBinary = false;
+
+ private DataSource mDataSource = null;
+ private DataDestination mDataDestination = null;
+
+ private long mUnknownSignatureKeyId = 0;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.decrypt);
+
+ mSource = (ViewFlipper) findViewById(R.id.source);
+ mSourceLabel = (TextView) findViewById(R.id.sourceLabel);
+ mSourcePrevious = (ImageView) findViewById(R.id.sourcePrevious);
+ mSourceNext = (ImageView) findViewById(R.id.sourceNext);
+
+ mSourcePrevious.setClickable(true);
+ mSourcePrevious.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ mSource.setInAnimation(AnimationUtils.loadAnimation(DecryptActivity.this,
+ R.anim.push_right_in));
+ mSource.setOutAnimation(AnimationUtils.loadAnimation(DecryptActivity.this,
+ R.anim.push_right_out));
+ mSource.showPrevious();
+ updateSource();
+ }
+ });
+
+ mSourceNext.setClickable(true);
+ OnClickListener nextSourceClickListener = new OnClickListener() {
+ public void onClick(View v) {
+ mSource.setInAnimation(AnimationUtils.loadAnimation(DecryptActivity.this,
+ R.anim.push_left_in));
+ mSource.setOutAnimation(AnimationUtils.loadAnimation(DecryptActivity.this,
+ R.anim.push_left_out));
+ mSource.showNext();
+ updateSource();
+ }
+ };
+ mSourceNext.setOnClickListener(nextSourceClickListener);
+
+ mSourceLabel.setClickable(true);
+ mSourceLabel.setOnClickListener(nextSourceClickListener);
+
+ mMessage = (EditText) findViewById(R.id.message);
+ mDecryptButton = (Button) findViewById(R.id.btn_decrypt);
+ mReplyButton = (Button) findViewById(R.id.btn_reply);
+ mSignatureLayout = (LinearLayout) findViewById(R.id.signature);
+ mSignatureStatusImage = (ImageView) findViewById(R.id.ic_signature_status);
+ mUserId = (TextView) findViewById(R.id.mainUserId);
+ mUserIdRest = (TextView) findViewById(R.id.mainUserIdRest);
+
+ // measure the height of the source_file view and set the message view's min height to that,
+ // so it fills mSource fully... bit of a hack.
+ View tmp = findViewById(R.id.sourceFile);
+ tmp.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+ int height = tmp.getMeasuredHeight();
+ mMessage.setMinimumHeight(height);
+
+ mFilename = (EditText) findViewById(R.id.filename);
+ mBrowse = (ImageButton) findViewById(R.id.btn_browse);
+ mBrowse.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ openFile();
+ }
+ });
+
+ mDeleteAfter = (CheckBox) findViewById(R.id.deleteAfterDecryption);
+
+ // default: message source
+ mSource.setInAnimation(null);
+ mSource.setOutAnimation(null);
+ while (mSource.getCurrentView().getId() != R.id.sourceMessage) {
+ mSource.showNext();
+ }
+
+ mIntent = getIntent();
+ if (Intent.ACTION_VIEW.equals(mIntent.getAction())) {
+ Uri uri = mIntent.getData();
+ try {
+ InputStream attachment = getContentResolver().openInputStream(uri);
+ ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
+ byte bytes[] = new byte[1 << 16];
+ int length;
+ while ((length = attachment.read(bytes)) > 0) {
+ byteOut.write(bytes, 0, length);
+ }
+ byteOut.close();
+ String data = new String(byteOut.toByteArray());
+ mMessage.setText(data);
+ } catch (FileNotFoundException e) {
+ // ignore, then
+ } catch (IOException e) {
+ // ignore, then
+ }
+ } else if (Apg.Intent.DECRYPT.equals(mIntent.getAction())) {
+ Log.d(Constants.TAG, "Apg Intent DECRYPT startet");
+ Bundle extras = mIntent.getExtras();
+ if (extras == null) {
+ Log.d(Constants.TAG, "extra bundle was null");
+ extras = new Bundle();
+ } else {
+ Log.d(Constants.TAG, "got extras");
+ }
+
+ mData = extras.getByteArray(Apg.EXTRA_DATA);
+ String textData = null;
+ if (mData == null) {
+ Log.d(Constants.TAG, "EXTRA_DATA was null");
+ textData = extras.getString(Apg.EXTRA_TEXT);
+ } else {
+ Log.d(Constants.TAG, "Got data from EXTRA_DATA");
+ }
+ if (textData != null) {
+ Log.d(Constants.TAG, "textData null, matching text ...");
+ Matcher matcher = Apg.PGP_MESSAGE.matcher(textData);
+ if (matcher.matches()) {
+ Log.d(Constants.TAG, "PGP_MESSAGE matched");
+ textData = matcher.group(1);
+ // replace non breakable spaces
+ textData = textData.replaceAll("\\xa0", " ");
+ mMessage.setText(textData);
+ } else {
+ matcher = Apg.PGP_SIGNED_MESSAGE.matcher(textData);
+ if (matcher.matches()) {
+ Log.d(Constants.TAG, "PGP_SIGNED_MESSAGE matched");
+ textData = matcher.group(1);
+ // replace non breakable spaces
+ textData = textData.replaceAll("\\xa0", " ");
+ mMessage.setText(textData);
+ mDecryptButton.setText(R.string.btn_verify);
+ } else {
+ Log.d(Constants.TAG, "Nothing matched!");
+ }
+ }
+ }
+ mReplyTo = extras.getString(Apg.EXTRA_REPLY_TO);
+ mSubject = extras.getString(Apg.EXTRA_SUBJECT);
+ } else if (Apg.Intent.DECRYPT_FILE.equals(mIntent.getAction())) {
+ mInputFilename = mIntent.getDataString();
+ if ("file".equals(mIntent.getScheme())) {
+ mInputFilename = Uri.decode(mInputFilename.substring(7));
+ }
+ mFilename.setText(mInputFilename);
+ guessOutputFilename();
+ mSource.setInAnimation(null);
+ mSource.setOutAnimation(null);
+ while (mSource.getCurrentView().getId() != R.id.sourceFile) {
+ mSource.showNext();
+ }
+ } else if (Apg.Intent.DECRYPT_AND_RETURN.equals(mIntent.getAction())) {
+ mContentUri = mIntent.getData();
+ Bundle extras = mIntent.getExtras();
+ if (extras == null) {
+ extras = new Bundle();
+ }
+
+ mReturnBinary = extras.getBoolean(Apg.EXTRA_BINARY, false);
+
+ if (mContentUri == null) {
+ mData = extras.getByteArray(Apg.EXTRA_DATA);
+ String data = extras.getString(Apg.EXTRA_TEXT);
+ if (data != null) {
+ Matcher matcher = Apg.PGP_MESSAGE.matcher(data);
+ if (matcher.matches()) {
+ data = matcher.group(1);
+ // replace non breakable spaces
+ data = data.replaceAll("\\xa0", " ");
+ mMessage.setText(data);
+ } else {
+ matcher = Apg.PGP_SIGNED_MESSAGE.matcher(data);
+ if (matcher.matches()) {
+ data = matcher.group(1);
+ // replace non breakable spaces
+ data = data.replaceAll("\\xa0", " ");
+ mMessage.setText(data);
+ mDecryptButton.setText(R.string.btn_verify);
+ }
+ }
+ }
+ }
+ mReturnResult = true;
+ }
+
+ if (mSource.getCurrentView().getId() == R.id.sourceMessage
+ && mMessage.getText().length() == 0) {
+
+ CharSequence clipboardText = Compatibility.getClipboardText(this);
+
+ String data = "";
+ if (clipboardText != null) {
+ Matcher matcher = Apg.PGP_MESSAGE.matcher(clipboardText);
+ if (!matcher.matches()) {
+ matcher = Apg.PGP_SIGNED_MESSAGE.matcher(clipboardText);
+ }
+ if (matcher.matches()) {
+ data = matcher.group(1);
+ mMessage.setText(data);
+ Toast.makeText(this, R.string.usingClipboardContent, Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+
+ mSignatureLayout.setVisibility(View.GONE);
+ mSignatureLayout.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ if (mSignatureKeyId == 0) {
+ return;
+ }
+ PGPPublicKeyRing key = Apg.getPublicKeyRing(mSignatureKeyId);
+ if (key != null) {
+ Intent intent = new Intent(DecryptActivity.this, KeyServerQueryActivity.class);
+ intent.setAction(Apg.Intent.LOOK_UP_KEY_ID);
+ intent.putExtra(Apg.EXTRA_KEY_ID, mSignatureKeyId);
+ startActivity(intent);
+ }
+ }
+ });
+
+ mDecryptButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ decryptClicked();
+ }
+ });
+
+ mReplyButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ replyClicked();
+ }
+ });
+ mReplyButton.setVisibility(View.INVISIBLE);
+
+ if (mReturnResult) {
+ mSourcePrevious.setClickable(false);
+ mSourcePrevious.setEnabled(false);
+ mSourcePrevious.setVisibility(View.INVISIBLE);
+
+ mSourceNext.setClickable(false);
+ mSourceNext.setEnabled(false);
+ mSourceNext.setVisibility(View.INVISIBLE);
+
+ mSourceLabel.setClickable(false);
+ mSourceLabel.setEnabled(false);
+ }
+
+ updateSource();
+
+ if (mSource.getCurrentView().getId() == R.id.sourceMessage
+ && (mMessage.getText().length() > 0 || mData != null || mContentUri != null)) {
+ mDecryptButton.performClick();
+ }
+ }
+
+ private void openFile() {
+ String filename = mFilename.getText().toString();
+
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+
+ intent.setData(Uri.parse("file://" + filename));
+ intent.setType("*/*");
+
+ try {
+ startActivityForResult(intent, Id.request.filename);
+ } catch (ActivityNotFoundException e) {
+ // No compatible file manager was found.
+ Toast.makeText(this, R.string.noFilemanagerInstalled, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void guessOutputFilename() {
+ mInputFilename = mFilename.getText().toString();
+ File file = new File(mInputFilename);
+ String filename = file.getName();
+ if (filename.endsWith(".asc") || filename.endsWith(".gpg") || filename.endsWith(".pgp")) {
+ filename = filename.substring(0, filename.length() - 4);
+ }
+ mOutputFilename = Constants.path.APP_DIR + "/" + filename;
+ }
+
+ private void updateSource() {
+ switch (mSource.getCurrentView().getId()) {
+ case R.id.sourceFile: {
+ mSourceLabel.setText(R.string.label_file);
+ mDecryptButton.setText(R.string.btn_decrypt);
+ break;
+ }
+
+ case R.id.sourceMessage: {
+ mSourceLabel.setText(R.string.label_message);
+ mDecryptButton.setText(R.string.btn_decrypt);
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ }
+
+ private void decryptClicked() {
+ if (mSource.getCurrentView().getId() == R.id.sourceFile) {
+ mDecryptTarget = Id.target.file;
+ } else {
+ mDecryptTarget = Id.target.message;
+ }
+ initiateDecryption();
+ }
+
+ private void initiateDecryption() {
+ if (mDecryptTarget == Id.target.file) {
+ String currentFilename = mFilename.getText().toString();
+ if (mInputFilename == null || !mInputFilename.equals(currentFilename)) {
+ guessOutputFilename();
+ }
+
+ if (mInputFilename.equals("")) {
+ Toast.makeText(this, R.string.noFileSelected, Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (mInputFilename.startsWith("file")) {
+ File file = new File(mInputFilename);
+ if (!file.exists() || !file.isFile()) {
+ Toast.makeText(
+ this,
+ getString(R.string.errorMessage, getString(R.string.error_fileNotFound)),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ }
+ }
+
+ if (mDecryptTarget == Id.target.message) {
+ String messageData = mMessage.getText().toString();
+ Matcher matcher = Apg.PGP_SIGNED_MESSAGE.matcher(messageData);
+ if (matcher.matches()) {
+ mSignedOnly = true;
+ decryptStart();
+ return;
+ }
+ }
+
+ // else treat it as an decrypted message/file
+ mSignedOnly = false;
+ String error = null;
+ fillDataSource();
+ try {
+ InputData in = mDataSource.getInputData(this, false);
+ try {
+ setSecretKeyId(Apg.getDecryptionKeyId(this, in));
+ if (getSecretKeyId() == Id.key.none) {
+ throw new Apg.GeneralException(getString(R.string.error_noSecretKeyFound));
+ }
+ mAssumeSymmetricEncryption = false;
+ } catch (Apg.NoAsymmetricEncryptionException e) {
+ setSecretKeyId(Id.key.symmetric);
+ in = mDataSource.getInputData(this, false);
+ if (!Apg.hasSymmetricEncryption(this, in)) {
+ throw new Apg.GeneralException(getString(R.string.error_noKnownEncryptionFound));
+ }
+ mAssumeSymmetricEncryption = true;
+ }
+
+ if (getSecretKeyId() == Id.key.symmetric
+ || Apg.getCachedPassPhrase(getSecretKeyId()) == null) {
+ showDialog(Id.dialog.pass_phrase);
+ } else {
+ if (mDecryptTarget == Id.target.file) {
+ askForOutputFilename();
+ } else {
+ decryptStart();
+ }
+ }
+ } catch (FileNotFoundException e) {
+ error = getString(R.string.error_fileNotFound);
+ } catch (IOException e) {
+ error = "" + e;
+ } catch (Apg.GeneralException e) {
+ error = "" + e;
+ }
+ if (error != null) {
+ Toast.makeText(this, getString(R.string.errorMessage, error), Toast.LENGTH_SHORT)
+ .show();
+ }
+ }
+
+ private void replyClicked() {
+ Intent intent = new Intent(this, EncryptActivity.class);
+ intent.setAction(Apg.Intent.ENCRYPT);
+ String data = mMessage.getText().toString();
+ data = data.replaceAll("(?m)^", "> ");
+ data = "\n\n" + data;
+ intent.putExtra(Apg.EXTRA_TEXT, data);
+ intent.putExtra(Apg.EXTRA_SUBJECT, "Re: " + mSubject);
+ intent.putExtra(Apg.EXTRA_SEND_TO, mReplyTo);
+ intent.putExtra(Apg.EXTRA_SIGNATURE_KEY_ID, getSecretKeyId());
+ intent.putExtra(Apg.EXTRA_ENCRYPTION_KEY_IDS, new long[] { mSignatureKeyId });
+ startActivity(intent);
+ }
+
+ private void askForOutputFilename() {
+ showDialog(Id.dialog.output_filename);
+ }
+
+ @Override
+ public void passPhraseCallback(long keyId, String passPhrase) {
+ super.passPhraseCallback(keyId, passPhrase);
+ if (mDecryptTarget == Id.target.file) {
+ askForOutputFilename();
+ } else {
+ decryptStart();
+ }
+ }
+
+ private void decryptStart() {
+ showDialog(Id.dialog.decrypting);
+ startThread();
+ }
+
+ @Override
+ public void run() {
+ String error = null;
+ Security.addProvider(new BouncyCastleProvider());
+
+ Bundle data = new Bundle();
+ Message msg = new Message();
+ fillDataSource();
+ fillDataDestination();
+ try {
+ InputData in = mDataSource.getInputData(this, true);
+ OutputStream out = mDataDestination.getOutputStream(this);
+
+ if (mSignedOnly) {
+ data = Apg.verifyText(this, in, out, this);
+ } else {
+ data = Apg.decrypt(this, in, out, Apg.getCachedPassPhrase(getSecretKeyId()), this,
+ mAssumeSymmetricEncryption);
+ }
+
+ out.close();
+
+ if (mDataDestination.getStreamFilename() != null) {
+ data.putString(Apg.EXTRA_RESULT_URI, "content://" + DataProvider.AUTHORITY
+ + "/data/" + mDataDestination.getStreamFilename());
+ } else if (mDecryptTarget == Id.target.message) {
+ if (mReturnBinary) {
+ data.putByteArray(Apg.EXTRA_DECRYPTED_DATA,
+ ((ByteArrayOutputStream) out).toByteArray());
+ } else {
+ data.putString(Apg.EXTRA_DECRYPTED_MESSAGE, new String(
+ ((ByteArrayOutputStream) out).toByteArray()));
+ }
+ }
+ } catch (PGPException e) {
+ error = "" + e;
+ } catch (IOException e) {
+ error = "" + e;
+ } catch (SignatureException e) {
+ error = "" + e;
+ } catch (Apg.GeneralException e) {
+ error = "" + e;
+ }
+
+ data.putInt(Constants.extras.STATUS, Id.message.done);
+
+ if (error != null) {
+ data.putString(Apg.EXTRA_ERROR, error);
+ }
+
+ msg.setData(data);
+ sendMessage(msg);
+ }
+
+ @Override
+ public void handlerCallback(Message msg) {
+ Bundle data = msg.getData();
+ if (data == null) {
+ return;
+ }
+
+ if (data.getInt(Constants.extras.STATUS) == Id.message.unknown_signature_key) {
+ mUnknownSignatureKeyId = data.getLong(Constants.extras.KEY_ID);
+ showDialog(Id.dialog.lookup_unknown_key);
+ return;
+ }
+
+ super.handlerCallback(msg);
+ }
+
+ @Override
+ public void doneCallback(Message msg) {
+ super.doneCallback(msg);
+
+ Bundle data = msg.getData();
+ removeDialog(Id.dialog.decrypting);
+ mSignatureKeyId = 0;
+ mSignatureLayout.setVisibility(View.GONE);
+ mReplyButton.setVisibility(View.INVISIBLE);
+
+ String error = data.getString(Apg.EXTRA_ERROR);
+ if (error != null) {
+ Toast.makeText(this, getString(R.string.errorMessage, error), Toast.LENGTH_SHORT)
+ .show();
+ return;
+ }
+
+ Toast.makeText(this, R.string.decryptionSuccessful, Toast.LENGTH_SHORT).show();
+ if (mReturnResult) {
+ Intent intent = new Intent();
+ intent.putExtras(data);
+ setResult(RESULT_OK, intent);
+ finish();
+ return;
+ }
+
+ switch (mDecryptTarget) {
+ case Id.target.message: {
+ String decryptedMessage = data.getString(Apg.EXTRA_DECRYPTED_MESSAGE);
+ mMessage.setText(decryptedMessage);
+ mMessage.setHorizontallyScrolling(false);
+ mReplyButton.setVisibility(View.VISIBLE);
+ break;
+ }
+
+ case Id.target.file: {
+ if (mDeleteAfter.isChecked()) {
+ setDeleteFile(mInputFilename);
+ showDialog(Id.dialog.delete_file);
+ }
+ break;
+ }
+
+ default: {
+ // shouldn't happen
+ break;
+ }
+ }
+
+ if (data.getBoolean(Apg.EXTRA_SIGNATURE)) {
+ String userId = data.getString(Apg.EXTRA_SIGNATURE_USER_ID);
+ mSignatureKeyId = data.getLong(Apg.EXTRA_SIGNATURE_KEY_ID);
+ mUserIdRest.setText("id: " + Apg.getSmallFingerPrint(mSignatureKeyId));
+ if (userId == null) {
+ userId = getResources().getString(R.string.unknownUserId);
+ }
+ String chunks[] = userId.split(" <", 2);
+ userId = chunks[0];
+ if (chunks.length > 1) {
+ mUserIdRest.setText("<" + chunks[1]);
+ }
+ mUserId.setText(userId);
+
+ if (data.getBoolean(Apg.EXTRA_SIGNATURE_SUCCESS)) {
+ mSignatureStatusImage.setImageResource(R.drawable.overlay_ok);
+ } else if (data.getBoolean(Apg.EXTRA_SIGNATURE_UNKNOWN)) {
+ mSignatureStatusImage.setImageResource(R.drawable.overlay_error);
+ Toast.makeText(this, R.string.unknownSignatureKeyTouchToLookUp, Toast.LENGTH_LONG)
+ .show();
+ } else {
+ mSignatureStatusImage.setImageResource(R.drawable.overlay_error);
+ }
+ mSignatureLayout.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.request.filename: {
+ if (resultCode == RESULT_OK && data != null) {
+ String filename = data.getDataString();
+ if (filename != null) {
+ // Get rid of URI prefix:
+ if (filename.startsWith("file://")) {
+ filename = filename.substring(7);
+ }
+ // replace %20 and so on
+ filename = Uri.decode(filename);
+
+ mFilename.setText(filename);
+ }
+ }
+ return;
+ }
+
+ case Id.request.output_filename: {
+ if (resultCode == RESULT_OK && data != null) {
+ String filename = data.getDataString();
+ if (filename != null) {
+ // Get rid of URI prefix:
+ if (filename.startsWith("file://")) {
+ filename = filename.substring(7);
+ }
+ // replace %20 and so on
+ filename = Uri.decode(filename);
+
+ FileDialog.setFilename(filename);
+ }
+ }
+ return;
+ }
+
+ case Id.request.look_up_key_id: {
+ PausableThread thread = getRunningThread();
+ if (thread != null && thread.isPaused()) {
+ thread.unpause();
+ }
+ return;
+ }
+
+ default: {
+ break;
+ }
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case Id.dialog.output_filename: {
+ return FileDialog.build(this, getString(R.string.title_decryptToFile),
+ getString(R.string.specifyFileToDecryptTo), mOutputFilename,
+ new FileDialog.OnClickListener() {
+ public void onOkClick(String filename, boolean checked) {
+ removeDialog(Id.dialog.output_filename);
+ mOutputFilename = filename;
+ decryptStart();
+ }
+
+ public void onCancelClick() {
+ removeDialog(Id.dialog.output_filename);
+ }
+ }, getString(R.string.filemanager_titleSave),
+ getString(R.string.filemanager_btnSave), null, Id.request.output_filename);
+ }
+
+ case Id.dialog.lookup_unknown_key: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ alert.setIcon(android.R.drawable.ic_dialog_alert);
+ alert.setTitle(R.string.title_unknownSignatureKey);
+ alert.setMessage(getString(R.string.lookupUnknownKey,
+ Apg.getSmallFingerPrint(mUnknownSignatureKeyId)));
+
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(Id.dialog.lookup_unknown_key);
+ Intent intent = new Intent(DecryptActivity.this, KeyServerQueryActivity.class);
+ intent.setAction(Apg.Intent.LOOK_UP_KEY_ID);
+ intent.putExtra(Apg.EXTRA_KEY_ID, mUnknownSignatureKeyId);
+ startActivityForResult(intent, Id.request.look_up_key_id);
+ }
+ });
+ alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(Id.dialog.lookup_unknown_key);
+ PausableThread thread = getRunningThread();
+ if (thread != null && thread.isPaused()) {
+ thread.unpause();
+ }
+ }
+ });
+ alert.setCancelable(true);
+
+ return alert.create();
+ }
+
+ default: {
+ break;
+ }
+ }
+
+ return super.onCreateDialog(id);
+ }
+
+ protected void fillDataSource() {
+ mDataSource = new DataSource();
+ if (mContentUri != null) {
+ mDataSource.setUri(mContentUri);
+ } else if (mDecryptTarget == Id.target.file) {
+ mDataSource.setUri(mInputFilename);
+ } else {
+ if (mData != null) {
+ mDataSource.setData(mData);
+ } else {
+ mDataSource.setText(mMessage.getText().toString());
+ }
+ }
+ }
+
+ protected void fillDataDestination() {
+ mDataDestination = new DataDestination();
+ if (mContentUri != null) {
+ mDataDestination.setMode(Id.mode.stream);
+ } else if (mDecryptTarget == Id.target.file) {
+ mDataDestination.setFilename(mOutputFilename);
+ mDataDestination.setMode(Id.mode.file);
+ } else {
+ mDataDestination.setMode(Id.mode.byte_array);
+ }
+ }
+}
diff --git a/org_apg/src/org/apg/ui/EditKeyActivity.java b/org_apg/src/org/apg/ui/EditKeyActivity.java
new file mode 100644
index 000000000..c3945d4ed
--- /dev/null
+++ b/org_apg/src/org/apg/ui/EditKeyActivity.java
@@ -0,0 +1,292 @@
+/*
+ * 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.apg.ui;
+
+import org.apg.Apg;
+import org.apg.Constants;
+import org.apg.Id;
+import org.apg.provider.Database;
+import org.apg.ui.widget.KeyEditor;
+import org.apg.ui.widget.SectionView;
+import org.apg.util.IterableIterator;
+import org.spongycastle.openpgp.PGPException;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.apg.R;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Message;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.Toast;
+
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SignatureException;
+import java.util.Vector;
+
+public class EditKeyActivity extends BaseActivity implements OnClickListener {
+
+ private PGPSecretKeyRing mKeyRing = null;
+
+ private SectionView mUserIds;
+ private SectionView mKeys;
+
+ private Button mSaveButton;
+ private Button mDiscardButton;
+
+ private String mCurrentPassPhrase = null;
+ private String mNewPassPhrase = null;
+
+ private Button mChangePassPhrase;
+
+ @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(Apg.EXTRA_KEY_ID);
+ }
+
+ 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);
+ }
+ }
+ }
+
+ mChangePassPhrase = (Button) findViewById(R.id.btn_change_pass_phrase);
+ mChangePassPhrase.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ showDialog(Id.dialog.new_pass_phrase);
+ }
+ });
+
+ 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(Id.type.user_id);
+ mUserIds.setUserIds(userIds);
+ container.addView(mUserIds);
+ mKeys = (SectionView) inflater.inflate(R.layout.edit_key_section, container, false);
+ mKeys.setType(Id.type.key);
+ mKeys.setKeys(keys);
+ container.addView(mKeys);
+
+ mCurrentPassPhrase = Apg.getEditPassPhrase();
+ if (mCurrentPassPhrase == null) {
+ mCurrentPassPhrase = "";
+ }
+
+ updatePassPhraseButtonText();
+
+ Toast.makeText(this,
+ getString(R.string.warningMessage, getString(R.string.keyEditingIsBeta)),
+ Toast.LENGTH_LONG).show();
+ }
+
+ private long getMasterKeyId() {
+ if (mKeys.getEditors().getChildCount() == 0) {
+ return 0;
+ }
+ return ((KeyEditor) mKeys.getEditors().getChildAt(0)).getValue().getKeyID();
+ }
+
+ public boolean havePassPhrase() {
+ return (!mCurrentPassPhrase.equals(""))
+ || (mNewPassPhrase != null && !mNewPassPhrase.equals(""));
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, Id.menu.option.preferences, 0, R.string.menu_preferences).setIcon(
+ android.R.drawable.ic_menu_preferences);
+ menu.add(0, Id.menu.option.about, 1, R.string.menu_about).setIcon(
+ android.R.drawable.ic_menu_info_details);
+ return true;
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case Id.dialog.new_pass_phrase: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ if (havePassPhrase()) {
+ alert.setTitle(R.string.title_changePassPhrase);
+ } else {
+ alert.setTitle(R.string.title_setPassPhrase);
+ }
+ alert.setMessage(R.string.enterPassPhraseTwice);
+
+ LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.pass_phrase, null);
+ final EditText input1 = (EditText) view.findViewById(R.id.passPhrase);
+ final EditText input2 = (EditText) view.findViewById(R.id.passPhraseAgain);
+
+ alert.setView(view);
+
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(Id.dialog.new_pass_phrase);
+
+ String passPhrase1 = "" + input1.getText();
+ String passPhrase2 = "" + input2.getText();
+ if (!passPhrase1.equals(passPhrase2)) {
+ showDialog(Id.dialog.pass_phrases_do_not_match);
+ return;
+ }
+
+ if (passPhrase1.equals("")) {
+ showDialog(Id.dialog.no_pass_phrase);
+ return;
+ }
+
+ mNewPassPhrase = passPhrase1;
+ updatePassPhraseButtonText();
+ }
+ });
+
+ alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(Id.dialog.new_pass_phrase);
+ }
+ });
+
+ return alert.create();
+ }
+
+ default: {
+ return super.onCreateDialog(id);
+ }
+ }
+ }
+
+ public void onClick(View v) {
+ if (v == mSaveButton) {
+ // TODO: some warning
+ saveClicked();
+ } else if (v == mDiscardButton) {
+ finish();
+ }
+ }
+
+ private void saveClicked() {
+ if (!havePassPhrase()) {
+ Toast.makeText(this, R.string.setAPassPhrase, Toast.LENGTH_SHORT).show();
+ return;
+ }
+ showDialog(Id.dialog.saving);
+ startThread();
+ }
+
+ @Override
+ public void run() {
+ String error = null;
+ Bundle data = new Bundle();
+ Message msg = new Message();
+
+ try {
+ String oldPassPhrase = mCurrentPassPhrase;
+ String newPassPhrase = mNewPassPhrase;
+ if (newPassPhrase == null) {
+ newPassPhrase = oldPassPhrase;
+ }
+ Apg.buildSecretKey(this, mUserIds, mKeys, oldPassPhrase, newPassPhrase, this);
+ Apg.setCachedPassPhrase(getMasterKeyId(), newPassPhrase);
+ } catch (NoSuchProviderException e) {
+ error = "" + e;
+ } catch (NoSuchAlgorithmException e) {
+ error = "" + e;
+ } catch (PGPException e) {
+ error = "" + e;
+ } catch (SignatureException e) {
+ error = "" + e;
+ } catch (Apg.GeneralException e) {
+ error = "" + e;
+ } catch (Database.GeneralException e) {
+ error = "" + e;
+ } catch (IOException e) {
+ error = "" + e;
+ }
+
+ data.putInt(Constants.extras.STATUS, Id.message.done);
+
+ if (error != null) {
+ data.putString(Apg.EXTRA_ERROR, error);
+ }
+
+ msg.setData(data);
+ sendMessage(msg);
+ }
+
+ @Override
+ public void doneCallback(Message msg) {
+ super.doneCallback(msg);
+
+ Bundle data = msg.getData();
+ removeDialog(Id.dialog.saving);
+
+ String error = data.getString(Apg.EXTRA_ERROR);
+ if (error != null) {
+ Toast.makeText(EditKeyActivity.this, getString(R.string.errorMessage, error),
+ Toast.LENGTH_SHORT).show();
+ } else {
+ Toast.makeText(EditKeyActivity.this, R.string.keySaved, Toast.LENGTH_SHORT).show();
+ setResult(RESULT_OK);
+ finish();
+ }
+ }
+
+ private void updatePassPhraseButtonText() {
+ mChangePassPhrase.setText(havePassPhrase() ? R.string.btn_changePassPhrase
+ : R.string.btn_setPassPhrase);
+ }
+}
diff --git a/org_apg/src/org/apg/ui/EncryptActivity.java b/org_apg/src/org/apg/ui/EncryptActivity.java
new file mode 100644
index 000000000..e5892a4d5
--- /dev/null
+++ b/org_apg/src/org/apg/ui/EncryptActivity.java
@@ -0,0 +1,998 @@
+/*
+ * 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.apg.ui;
+
+import org.apg.Apg;
+import org.apg.Constants;
+import org.apg.DataDestination;
+import org.apg.DataSource;
+import org.apg.FileDialog;
+import org.apg.Id;
+import org.apg.InputData;
+import org.apg.provider.DataProvider;
+import org.apg.util.Choice;
+import org.apg.util.Compatibility;
+import org.spongycastle.openpgp.PGPException;
+import org.spongycastle.openpgp.PGPPublicKey;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.apg.R;
+
+import android.app.Dialog;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Message;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.animation.AnimationUtils;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.ViewFlipper;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SignatureException;
+import java.util.Vector;
+
+public class EncryptActivity extends BaseActivity {
+ private Intent mIntent = null;
+ private String mSubject = null;
+ private String mSendTo = null;
+
+ private long mEncryptionKeyIds[] = null;
+
+ private boolean mReturnResult = false;
+ private EditText mMessage = null;
+ private Button mSelectKeysButton = null;
+ private Button mEncryptButton = null;
+ private Button mEncryptToClipboardButton = null;
+ private CheckBox mSign = null;
+ private TextView mMainUserId = null;
+ private TextView mMainUserIdRest = null;
+
+ private ViewFlipper mSource = null;
+ private TextView mSourceLabel = null;
+ private ImageView mSourcePrevious = null;
+ private ImageView mSourceNext = null;
+
+ private ViewFlipper mMode = null;
+ private TextView mModeLabel = null;
+ private ImageView mModePrevious = null;
+ private ImageView mModeNext = null;
+
+ private int mEncryptTarget;
+
+ private EditText mPassPhrase = null;
+ private EditText mPassPhraseAgain = null;
+ private CheckBox mAsciiArmour = null;
+ private Spinner mFileCompression = null;
+
+ private EditText mFilename = null;
+ private CheckBox mDeleteAfter = null;
+ private ImageButton mBrowse = null;
+
+ private String mInputFilename = null;
+ private String mOutputFilename = null;
+
+ private boolean mAsciiArmourDemand = false;
+ private boolean mOverrideAsciiArmour = false;
+ private Uri mContentUri = null;
+ private byte[] mData = null;
+
+ private DataSource mDataSource = null;
+ private DataDestination mDataDestination = null;
+
+ private boolean mGenerateSignature = false;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.encrypt);
+
+ mGenerateSignature = false;
+
+ mSource = (ViewFlipper) findViewById(R.id.source);
+ mSourceLabel = (TextView) findViewById(R.id.sourceLabel);
+ mSourcePrevious = (ImageView) findViewById(R.id.sourcePrevious);
+ mSourceNext = (ImageView) findViewById(R.id.sourceNext);
+
+ mSourcePrevious.setClickable(true);
+ mSourcePrevious.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ mSource.setInAnimation(AnimationUtils.loadAnimation(EncryptActivity.this,
+ R.anim.push_right_in));
+ mSource.setOutAnimation(AnimationUtils.loadAnimation(EncryptActivity.this,
+ R.anim.push_right_out));
+ mSource.showPrevious();
+ updateSource();
+ }
+ });
+
+ mSourceNext.setClickable(true);
+ OnClickListener nextSourceClickListener = new OnClickListener() {
+ public void onClick(View v) {
+ mSource.setInAnimation(AnimationUtils.loadAnimation(EncryptActivity.this,
+ R.anim.push_left_in));
+ mSource.setOutAnimation(AnimationUtils.loadAnimation(EncryptActivity.this,
+ R.anim.push_left_out));
+ mSource.showNext();
+ updateSource();
+ }
+ };
+ mSourceNext.setOnClickListener(nextSourceClickListener);
+
+ mSourceLabel.setClickable(true);
+ mSourceLabel.setOnClickListener(nextSourceClickListener);
+
+ mMode = (ViewFlipper) findViewById(R.id.mode);
+ mModeLabel = (TextView) findViewById(R.id.modeLabel);
+ mModePrevious = (ImageView) findViewById(R.id.modePrevious);
+ mModeNext = (ImageView) findViewById(R.id.modeNext);
+
+ mModePrevious.setClickable(true);
+ mModePrevious.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ mMode.setInAnimation(AnimationUtils.loadAnimation(EncryptActivity.this,
+ R.anim.push_right_in));
+ mMode.setOutAnimation(AnimationUtils.loadAnimation(EncryptActivity.this,
+ R.anim.push_right_out));
+ mMode.showPrevious();
+ updateMode();
+ }
+ });
+
+ OnClickListener nextModeClickListener = new OnClickListener() {
+ public void onClick(View v) {
+ mMode.setInAnimation(AnimationUtils.loadAnimation(EncryptActivity.this,
+ R.anim.push_left_in));
+ mMode.setOutAnimation(AnimationUtils.loadAnimation(EncryptActivity.this,
+ R.anim.push_left_out));
+ mMode.showNext();
+ updateMode();
+ }
+ };
+ mModeNext.setOnClickListener(nextModeClickListener);
+
+ mModeLabel.setClickable(true);
+ mModeLabel.setOnClickListener(nextModeClickListener);
+
+ mMessage = (EditText) findViewById(R.id.message);
+ mSelectKeysButton = (Button) findViewById(R.id.btn_selectEncryptKeys);
+ mEncryptButton = (Button) findViewById(R.id.btn_encrypt);
+ mEncryptToClipboardButton = (Button) findViewById(R.id.btn_encryptToClipboard);
+ mSign = (CheckBox) findViewById(R.id.sign);
+ mMainUserId = (TextView) findViewById(R.id.mainUserId);
+ mMainUserIdRest = (TextView) findViewById(R.id.mainUserIdRest);
+
+ mPassPhrase = (EditText) findViewById(R.id.passPhrase);
+ mPassPhraseAgain = (EditText) findViewById(R.id.passPhraseAgain);
+
+ // measure the height of the source_file view and set the message view's min height to that,
+ // so it fills mSource fully... bit of a hack.
+ View tmp = findViewById(R.id.sourceFile);
+ tmp.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+ int height = tmp.getMeasuredHeight();
+ mMessage.setMinimumHeight(height);
+
+ mFilename = (EditText) findViewById(R.id.filename);
+ mBrowse = (ImageButton) findViewById(R.id.btn_browse);
+ mBrowse.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ openFile();
+ }
+ });
+
+ mFileCompression = (Spinner) findViewById(R.id.fileCompression);
+ Choice[] choices = new Choice[] {
+ new Choice(Id.choice.compression.none, getString(R.string.choice_none) + " ("
+ + getString(R.string.fast) + ")"),
+ new Choice(Id.choice.compression.zip, "ZIP (" + getString(R.string.fast) + ")"),
+ new Choice(Id.choice.compression.zlib, "ZLIB (" + getString(R.string.fast) + ")"),
+ new Choice(Id.choice.compression.bzip2, "BZIP2 (" + getString(R.string.very_slow)
+ + ")"), };
+ ArrayAdapter<Choice> adapter = new ArrayAdapter<Choice>(this,
+ android.R.layout.simple_spinner_item, choices);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mFileCompression.setAdapter(adapter);
+
+ int defaultFileCompression = mPreferences.getDefaultFileCompression();
+ for (int i = 0; i < choices.length; ++i) {
+ if (choices[i].getId() == defaultFileCompression) {
+ mFileCompression.setSelection(i);
+ break;
+ }
+ }
+
+ mDeleteAfter = (CheckBox) findViewById(R.id.deleteAfterEncryption);
+
+ mAsciiArmour = (CheckBox) findViewById(R.id.asciiArmour);
+ mAsciiArmour.setChecked(mPreferences.getDefaultAsciiArmour());
+ mAsciiArmour.setOnClickListener(new OnClickListener() {
+ public void onClick(View view) {
+ guessOutputFilename();
+ }
+ });
+
+ mEncryptToClipboardButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ encryptToClipboardClicked();
+ }
+ });
+
+ mEncryptButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ encryptClicked();
+ }
+ });
+
+ mSelectKeysButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ selectPublicKeys();
+ }
+ });
+
+ mSign.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ CheckBox checkBox = (CheckBox) v;
+ if (checkBox.isChecked()) {
+ selectSecretKey();
+ } else {
+ setSecretKeyId(Id.key.none);
+ updateView();
+ }
+ }
+ });
+
+ mIntent = getIntent();
+ if (Apg.Intent.ENCRYPT.equals(mIntent.getAction())
+ || Apg.Intent.ENCRYPT_FILE.equals(mIntent.getAction())
+ || Apg.Intent.ENCRYPT_AND_RETURN.equals(mIntent.getAction())
+ || Apg.Intent.GENERATE_SIGNATURE.equals(mIntent.getAction())) {
+ mContentUri = mIntent.getData();
+ Bundle extras = mIntent.getExtras();
+ if (extras == null) {
+ extras = new Bundle();
+ }
+
+ if (Apg.Intent.ENCRYPT_AND_RETURN.equals(mIntent.getAction())
+ || Apg.Intent.GENERATE_SIGNATURE.equals(mIntent.getAction())) {
+ mReturnResult = true;
+ }
+
+ if (Apg.Intent.GENERATE_SIGNATURE.equals(mIntent.getAction())) {
+ mGenerateSignature = true;
+ mOverrideAsciiArmour = true;
+ mAsciiArmourDemand = false;
+ }
+
+ if (extras.containsKey(Apg.EXTRA_ASCII_ARMOUR)) {
+ mAsciiArmourDemand = extras.getBoolean(Apg.EXTRA_ASCII_ARMOUR, true);
+ mOverrideAsciiArmour = true;
+ mAsciiArmour.setChecked(mAsciiArmourDemand);
+ }
+
+ mData = extras.getByteArray(Apg.EXTRA_DATA);
+ String textData = null;
+ if (mData == null) {
+ textData = extras.getString(Apg.EXTRA_TEXT);
+ }
+ mSendTo = extras.getString(Apg.EXTRA_SEND_TO);
+ mSubject = extras.getString(Apg.EXTRA_SUBJECT);
+ long signatureKeyId = extras.getLong(Apg.EXTRA_SIGNATURE_KEY_ID);
+ long encryptionKeyIds[] = extras.getLongArray(Apg.EXTRA_ENCRYPTION_KEY_IDS);
+ if (signatureKeyId != 0) {
+ PGPSecretKeyRing keyRing = Apg.getSecretKeyRing(signatureKeyId);
+ PGPSecretKey masterKey = null;
+ if (keyRing != null) {
+ masterKey = Apg.getMasterKey(keyRing);
+ if (masterKey != null) {
+ Vector<PGPSecretKey> signKeys = Apg.getUsableSigningKeys(keyRing);
+ if (signKeys.size() > 0) {
+ setSecretKeyId(masterKey.getKeyID());
+ }
+ }
+ }
+ }
+
+ if (encryptionKeyIds != null) {
+ Vector<Long> goodIds = new Vector<Long>();
+ for (int i = 0; i < encryptionKeyIds.length; ++i) {
+ PGPPublicKeyRing keyRing = Apg.getPublicKeyRing(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 (Apg.Intent.ENCRYPT.equals(mIntent.getAction())
+ || Apg.Intent.ENCRYPT_AND_RETURN.equals(mIntent.getAction())
+ || Apg.Intent.GENERATE_SIGNATURE.equals(mIntent.getAction())) {
+ if (textData != null) {
+ mMessage.setText(textData);
+ }
+ mSource.setInAnimation(null);
+ mSource.setOutAnimation(null);
+ while (mSource.getCurrentView().getId() != R.id.sourceMessage) {
+ mSource.showNext();
+ }
+ } else if (Apg.Intent.ENCRYPT_FILE.equals(mIntent.getAction())) {
+ if ("file".equals(mIntent.getScheme())) {
+ mInputFilename = Uri.decode(mIntent.getDataString().replace("file://", ""));
+ mFilename.setText(mInputFilename);
+ guessOutputFilename();
+ }
+ mSource.setInAnimation(null);
+ mSource.setOutAnimation(null);
+ while (mSource.getCurrentView().getId() != R.id.sourceFile) {
+ mSource.showNext();
+ }
+ }
+ }
+
+ updateView();
+ updateSource();
+ updateMode();
+
+ if (mReturnResult) {
+ mSourcePrevious.setClickable(false);
+ mSourcePrevious.setEnabled(false);
+ mSourcePrevious.setVisibility(View.INVISIBLE);
+
+ mSourceNext.setClickable(false);
+ mSourceNext.setEnabled(false);
+ mSourceNext.setVisibility(View.INVISIBLE);
+
+ mSourceLabel.setClickable(false);
+ mSourceLabel.setEnabled(false);
+ }
+
+ updateButtons();
+
+ if (mReturnResult
+ && (mMessage.getText().length() > 0 || mData != null || mContentUri != null)
+ && ((mEncryptionKeyIds != null && mEncryptionKeyIds.length > 0) || getSecretKeyId() != 0)) {
+ encryptClicked();
+ }
+ }
+
+ private void openFile() {
+ String filename = mFilename.getText().toString();
+
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+
+ intent.setData(Uri.parse("file://" + filename));
+ intent.setType("*/*");
+
+ try {
+ startActivityForResult(intent, Id.request.filename);
+ } catch (ActivityNotFoundException e) {
+ // No compatible file manager was found.
+ Toast.makeText(this, R.string.noFilemanagerInstalled, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void guessOutputFilename() {
+ mInputFilename = mFilename.getText().toString();
+ File file = new File(mInputFilename);
+ String ending = (mAsciiArmour.isChecked() ? ".asc" : ".gpg");
+ mOutputFilename = Constants.path.APP_DIR + "/" + file.getName() + ending;
+ }
+
+ private void updateSource() {
+ switch (mSource.getCurrentView().getId()) {
+ case R.id.sourceFile: {
+ mSourceLabel.setText(R.string.label_file);
+ break;
+ }
+
+ case R.id.sourceMessage: {
+ mSourceLabel.setText(R.string.label_message);
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ updateButtons();
+ }
+
+ private void updateButtons() {
+ switch (mSource.getCurrentView().getId()) {
+ case R.id.sourceFile: {
+ mEncryptToClipboardButton.setVisibility(View.INVISIBLE);
+ mEncryptButton.setText(R.string.btn_encrypt);
+ break;
+ }
+
+ case R.id.sourceMessage: {
+ mSourceLabel.setText(R.string.label_message);
+ if (mReturnResult) {
+ mEncryptToClipboardButton.setVisibility(View.INVISIBLE);
+ } else {
+ mEncryptToClipboardButton.setVisibility(View.VISIBLE);
+ }
+ if (mMode.getCurrentView().getId() == R.id.modeSymmetric) {
+ if (mReturnResult) {
+ mEncryptButton.setText(R.string.btn_encrypt);
+ } else {
+ mEncryptButton.setText(R.string.btn_encryptAndEmail);
+ }
+ mEncryptButton.setEnabled(true);
+ mEncryptToClipboardButton.setText(R.string.btn_encryptToClipboard);
+ mEncryptToClipboardButton.setEnabled(true);
+ } else {
+ if (mEncryptionKeyIds == null || mEncryptionKeyIds.length == 0) {
+ if (getSecretKeyId() == 0) {
+ if (mReturnResult) {
+ mEncryptButton.setText(R.string.btn_encrypt);
+ } else {
+ mEncryptButton.setText(R.string.btn_encryptAndEmail);
+ }
+ mEncryptButton.setEnabled(false);
+ mEncryptToClipboardButton.setText(R.string.btn_encryptToClipboard);
+ mEncryptToClipboardButton.setEnabled(false);
+ } else {
+ if (mReturnResult) {
+ mEncryptButton.setText(R.string.btn_sign);
+ } else {
+ mEncryptButton.setText(R.string.btn_signAndEmail);
+ }
+ mEncryptButton.setEnabled(true);
+ mEncryptToClipboardButton.setText(R.string.btn_signToClipboard);
+ mEncryptToClipboardButton.setEnabled(true);
+ }
+ } else {
+ if (mReturnResult) {
+ mEncryptButton.setText(R.string.btn_encrypt);
+ } else {
+ mEncryptButton.setText(R.string.btn_encryptAndEmail);
+ }
+ mEncryptButton.setEnabled(true);
+ mEncryptToClipboardButton.setText(R.string.btn_encryptToClipboard);
+ mEncryptToClipboardButton.setEnabled(true);
+ }
+ }
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ }
+
+ private void updateMode() {
+ switch (mMode.getCurrentView().getId()) {
+ case R.id.modeAsymmetric: {
+ mModeLabel.setText(R.string.label_asymmetric);
+ break;
+ }
+
+ case R.id.modeSymmetric: {
+ mModeLabel.setText(R.string.label_symmetric);
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ updateButtons();
+ }
+
+ private void encryptToClipboardClicked() {
+ mEncryptTarget = Id.target.clipboard;
+ initiateEncryption();
+ }
+
+ private void encryptClicked() {
+ if (mSource.getCurrentView().getId() == R.id.sourceFile) {
+ mEncryptTarget = Id.target.file;
+ } else {
+ mEncryptTarget = Id.target.email;
+ }
+ initiateEncryption();
+ }
+
+ private void initiateEncryption() {
+ if (mEncryptTarget == Id.target.file) {
+ String currentFilename = mFilename.getText().toString();
+ if (mInputFilename == null || !mInputFilename.equals(currentFilename)) {
+ guessOutputFilename();
+ }
+
+ if (mInputFilename.equals("")) {
+ Toast.makeText(this, R.string.noFileSelected, Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (!mInputFilename.startsWith("content")) {
+ File file = new File(mInputFilename);
+ if (!file.exists() || !file.isFile()) {
+ Toast.makeText(
+ this,
+ getString(R.string.errorMessage, getString(R.string.error_fileNotFound)),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ }
+ }
+
+ // symmetric encryption
+ if (mMode.getCurrentView().getId() == R.id.modeSymmetric) {
+ boolean gotPassPhrase = false;
+ String passPhrase = mPassPhrase.getText().toString();
+ String passPhraseAgain = mPassPhraseAgain.getText().toString();
+ if (!passPhrase.equals(passPhraseAgain)) {
+ Toast.makeText(this, R.string.passPhrasesDoNotMatch, Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ gotPassPhrase = (passPhrase.length() != 0);
+ if (!gotPassPhrase) {
+ Toast.makeText(this, R.string.passPhraseMustNotBeEmpty, Toast.LENGTH_SHORT).show();
+ return;
+ }
+ } else {
+ boolean encryptIt = (mEncryptionKeyIds != null && mEncryptionKeyIds.length > 0);
+ // for now require at least one form of encryption for files
+ if (!encryptIt && mEncryptTarget == Id.target.file) {
+ Toast.makeText(this, R.string.selectEncryptionKey, Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (!encryptIt && getSecretKeyId() == 0) {
+ Toast.makeText(this, R.string.selectEncryptionOrSignatureKey, Toast.LENGTH_SHORT)
+ .show();
+ return;
+ }
+
+ if (getSecretKeyId() != 0 && Apg.getCachedPassPhrase(getSecretKeyId()) == null) {
+ showDialog(Id.dialog.pass_phrase);
+ return;
+ }
+ }
+
+ if (mEncryptTarget == Id.target.file) {
+ askForOutputFilename();
+ } else {
+ encryptStart();
+ }
+ }
+
+ private void askForOutputFilename() {
+ showDialog(Id.dialog.output_filename);
+ }
+
+ @Override
+ public void passPhraseCallback(long keyId, String passPhrase) {
+ super.passPhraseCallback(keyId, passPhrase);
+ if (mEncryptTarget == Id.target.file) {
+ askForOutputFilename();
+ } else {
+ encryptStart();
+ }
+ }
+
+ private void encryptStart() {
+ showDialog(Id.dialog.encrypting);
+ startThread();
+ }
+
+ @Override
+ public void run() {
+ String error = null;
+ Bundle data = new Bundle();
+ Message msg = new Message();
+
+ try {
+ InputData in;
+ OutputStream out;
+ boolean useAsciiArmour = true;
+ long encryptionKeyIds[] = null;
+ long signatureKeyId = 0;
+ int compressionId = 0;
+ boolean signOnly = false;
+
+ String passPhrase = null;
+ if (mMode.getCurrentView().getId() == R.id.modeSymmetric) {
+ passPhrase = mPassPhrase.getText().toString();
+ if (passPhrase.length() == 0) {
+ passPhrase = null;
+ }
+ } else {
+ encryptionKeyIds = mEncryptionKeyIds;
+ signatureKeyId = getSecretKeyId();
+ signOnly = (mEncryptionKeyIds == null || mEncryptionKeyIds.length == 0);
+ }
+
+ fillDataSource(signOnly && !mReturnResult);
+ fillDataDestination();
+
+ // streams
+ in = mDataSource.getInputData(this, true);
+ out = mDataDestination.getOutputStream(this);
+
+ if (mEncryptTarget == Id.target.file) {
+ useAsciiArmour = mAsciiArmour.isChecked();
+ compressionId = ((Choice) mFileCompression.getSelectedItem()).getId();
+ } else {
+ useAsciiArmour = true;
+ compressionId = mPreferences.getDefaultMessageCompression();
+ }
+
+ if (mOverrideAsciiArmour) {
+ useAsciiArmour = mAsciiArmourDemand;
+ }
+
+ if (mGenerateSignature) {
+ Apg.generateSignature(this, in, out, useAsciiArmour, mDataSource.isBinary(),
+ getSecretKeyId(), Apg.getCachedPassPhrase(getSecretKeyId()),
+ mPreferences.getDefaultHashAlgorithm(),
+ mPreferences.getForceV3Signatures(), this);
+ } else if (signOnly) {
+ Apg.signText(this, in, out, getSecretKeyId(),
+ Apg.getCachedPassPhrase(getSecretKeyId()),
+ mPreferences.getDefaultHashAlgorithm(),
+ mPreferences.getForceV3Signatures(), this);
+ } else {
+ Apg.encrypt(this, in, out, useAsciiArmour, encryptionKeyIds, signatureKeyId,
+ Apg.getCachedPassPhrase(signatureKeyId), this,
+ mPreferences.getDefaultEncryptionAlgorithm(),
+ mPreferences.getDefaultHashAlgorithm(), compressionId,
+ mPreferences.getForceV3Signatures(), passPhrase);
+ }
+
+ out.close();
+ if (mEncryptTarget != Id.target.file) {
+
+ if (out instanceof ByteArrayOutputStream) {
+ if (useAsciiArmour) {
+ String extraData = new String(((ByteArrayOutputStream) out).toByteArray());
+ if (mGenerateSignature) {
+ data.putString(Apg.EXTRA_SIGNATURE_TEXT, extraData);
+ } else {
+ data.putString(Apg.EXTRA_ENCRYPTED_MESSAGE, extraData);
+ }
+ } else {
+ byte extraData[] = ((ByteArrayOutputStream) out).toByteArray();
+ if (mGenerateSignature) {
+ data.putByteArray(Apg.EXTRA_SIGNATURE_DATA, extraData);
+ } else {
+ data.putByteArray(Apg.EXTRA_ENCRYPTED_DATA, extraData);
+ }
+ }
+ } else if (out instanceof FileOutputStream) {
+ String fileName = mDataDestination.getStreamFilename();
+ String uri = "content://" + DataProvider.AUTHORITY + "/data/" + fileName;
+ data.putString(Apg.EXTRA_RESULT_URI, uri);
+ } else {
+ throw new Apg.GeneralException("No output-data found.");
+ }
+ }
+ } catch (IOException e) {
+ error = "" + e;
+ } catch (PGPException e) {
+ error = "" + e;
+ } catch (NoSuchProviderException e) {
+ error = "" + e;
+ } catch (NoSuchAlgorithmException e) {
+ error = "" + e;
+ } catch (SignatureException e) {
+ error = "" + e;
+ } catch (Apg.GeneralException e) {
+ error = "" + e;
+ }
+
+ data.putInt(Constants.extras.STATUS, Id.message.done);
+
+ if (error != null) {
+ data.putString(Apg.EXTRA_ERROR, error);
+ }
+
+ msg.setData(data);
+ sendMessage(msg);
+ }
+
+ private void updateView() {
+ if (mEncryptionKeyIds == null || mEncryptionKeyIds.length == 0) {
+ mSelectKeysButton.setText(R.string.noKeysSelected);
+ } else if (mEncryptionKeyIds.length == 1) {
+ mSelectKeysButton.setText(R.string.oneKeySelected);
+ } else {
+ mSelectKeysButton.setText("" + mEncryptionKeyIds.length + " "
+ + getResources().getString(R.string.nKeysSelected));
+ }
+
+ if (getSecretKeyId() == 0) {
+ mSign.setChecked(false);
+ mMainUserId.setText("");
+ mMainUserIdRest.setText("");
+ } else {
+ String uid = getResources().getString(R.string.unknownUserId);
+ String uidExtra = "";
+ PGPSecretKeyRing keyRing = Apg.getSecretKeyRing(getSecretKeyId());
+ 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.setChecked(true);
+ }
+
+ updateButtons();
+ }
+
+ private void selectPublicKeys() {
+ Intent intent = new Intent(this, SelectPublicKeyListActivity.class);
+ Vector<Long> keyIds = new Vector<Long>();
+ if (getSecretKeyId() != 0) {
+ keyIds.add(getSecretKeyId());
+ }
+ if (mEncryptionKeyIds != null && mEncryptionKeyIds.length > 0) {
+ for (int i = 0; i < mEncryptionKeyIds.length; ++i) {
+ keyIds.add(mEncryptionKeyIds[i]);
+ }
+ }
+ long[] initialKeyIds = null;
+ if (keyIds.size() > 0) {
+ initialKeyIds = new long[keyIds.size()];
+ for (int i = 0; i < keyIds.size(); ++i) {
+ initialKeyIds[i] = keyIds.get(i);
+ }
+ }
+ intent.putExtra(Apg.EXTRA_SELECTION, initialKeyIds);
+ startActivityForResult(intent, Id.request.public_keys);
+ }
+
+ private void selectSecretKey() {
+ Intent intent = new Intent(this, SelectSecretKeyListActivity.class);
+ startActivityForResult(intent, Id.request.secret_keys);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.request.filename: {
+ if (resultCode == RESULT_OK && data != null) {
+ String filename = data.getDataString();
+ if (filename != null) {
+ // Get rid of URI prefix:
+ if (filename.startsWith("file://")) {
+ filename = filename.substring(7);
+ }
+ // replace %20 and so on
+ filename = Uri.decode(filename);
+
+ mFilename.setText(filename);
+ }
+ }
+ return;
+ }
+
+ case Id.request.output_filename: {
+ if (resultCode == RESULT_OK && data != null) {
+ String filename = data.getDataString();
+ if (filename != null) {
+ // Get rid of URI prefix:
+ if (filename.startsWith("file://")) {
+ filename = filename.substring(7);
+ }
+ // replace %20 and so on
+ filename = Uri.decode(filename);
+
+ FileDialog.setFilename(filename);
+ }
+ }
+ return;
+ }
+
+ case Id.request.secret_keys: {
+ if (resultCode == RESULT_OK) {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ updateView();
+ break;
+ }
+
+ case Id.request.public_keys: {
+ if (resultCode == RESULT_OK) {
+ Bundle bundle = data.getExtras();
+ mEncryptionKeyIds = bundle.getLongArray(Apg.EXTRA_SELECTION);
+ }
+ updateView();
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ @Override
+ public void doneCallback(Message msg) {
+ super.doneCallback(msg);
+
+ removeDialog(Id.dialog.encrypting);
+
+ Bundle data = msg.getData();
+ String error = data.getString(Apg.EXTRA_ERROR);
+ if (error != null) {
+ Toast.makeText(this, getString(R.string.errorMessage, error), Toast.LENGTH_SHORT)
+ .show();
+ return;
+ }
+ switch (mEncryptTarget) {
+ case Id.target.clipboard: {
+ String message = data.getString(Apg.EXTRA_ENCRYPTED_MESSAGE);
+ Compatibility.copyToClipboard(this, message);
+ Toast.makeText(this, R.string.encryptionToClipboardSuccessful, Toast.LENGTH_SHORT)
+ .show();
+ break;
+ }
+
+ case Id.target.email: {
+ if (mReturnResult) {
+ Intent intent = new Intent();
+ intent.putExtras(data);
+ setResult(RESULT_OK, intent);
+ finish();
+ return;
+ }
+
+ String message = data.getString(Apg.EXTRA_ENCRYPTED_MESSAGE);
+ 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 (mSubject != null) {
+ emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, mSubject);
+ }
+ if (mSendTo != null) {
+ emailIntent.putExtra(android.content.Intent.EXTRA_EMAIL, new String[] { mSendTo });
+ }
+ EncryptActivity.this.startActivity(Intent.createChooser(emailIntent,
+ getString(R.string.title_sendEmail)));
+ break;
+ }
+
+ case Id.target.file: {
+ Toast.makeText(this, R.string.encryptionSuccessful, Toast.LENGTH_SHORT).show();
+ if (mDeleteAfter.isChecked()) {
+ setDeleteFile(mInputFilename);
+ showDialog(Id.dialog.delete_file);
+ }
+ break;
+ }
+
+ default: {
+ // shouldn't happen
+ break;
+ }
+ }
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case Id.dialog.output_filename: {
+ return FileDialog.build(this, getString(R.string.title_encryptToFile),
+ getString(R.string.specifyFileToEncryptTo), mOutputFilename,
+ new FileDialog.OnClickListener() {
+ public void onOkClick(String filename, boolean checked) {
+ removeDialog(Id.dialog.output_filename);
+ mOutputFilename = filename;
+ encryptStart();
+ }
+
+ public void onCancelClick() {
+ removeDialog(Id.dialog.output_filename);
+ }
+ }, getString(R.string.filemanager_titleSave),
+ getString(R.string.filemanager_btnSave), null, Id.request.output_filename);
+ }
+
+ default: {
+ break;
+ }
+ }
+
+ return super.onCreateDialog(id);
+ }
+
+ protected void fillDataSource(boolean fixContent) {
+ mDataSource = new DataSource();
+ if (mContentUri != null) {
+ mDataSource.setUri(mContentUri);
+ } else if (mEncryptTarget == Id.target.file) {
+ mDataSource.setUri(mInputFilename);
+ } else {
+ if (mData != null) {
+ mDataSource.setData(mData);
+ } else {
+ String message = mMessage.getText().toString();
+ if (fixContent) {
+ // fix the message a bit, trailing spaces and newlines break stuff,
+ // because GMail sends as HTML and such things fuck up the
+ // signature,
+ // TODO: things like "<" and ">" also fuck up the signature
+ message = message.replaceAll(" +\n", "\n");
+ message = message.replaceAll("\n\n+", "\n\n");
+ message = message.replaceFirst("^\n+", "");
+ // make sure there'll be exactly one newline at the end
+ message = message.replaceFirst("\n*$", "\n");
+ }
+ mDataSource.setText(message);
+ }
+ }
+ }
+
+ protected void fillDataDestination() {
+ mDataDestination = new DataDestination();
+ if (mContentUri != null) {
+ mDataDestination.setMode(Id.mode.stream);
+ } else if (mEncryptTarget == Id.target.file) {
+ mDataDestination.setFilename(mOutputFilename);
+ mDataDestination.setMode(Id.mode.file);
+ } else {
+ mDataDestination.setMode(Id.mode.byte_array);
+ }
+ }
+}
diff --git a/org_apg/src/org/apg/ui/GeneralActivity.java b/org_apg/src/org/apg/ui/GeneralActivity.java
new file mode 100644
index 000000000..d70694630
--- /dev/null
+++ b/org_apg/src/org/apg/ui/GeneralActivity.java
@@ -0,0 +1,177 @@
+package org.apg.ui;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Vector;
+
+import org.apg.Apg;
+import org.apg.Id;
+import org.apg.util.Choice;
+import org.apg.R;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.Toast;
+
+public class GeneralActivity extends BaseActivity {
+ private Intent mIntent;
+ private ArrayAdapter<Choice> mAdapter;
+ private ListView mList;
+ private Button mCancelButton;
+ private String mDataString;
+ private Uri mDataUri;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.general);
+
+ mIntent = getIntent();
+
+ InputStream inStream = null;
+ {
+ String data = mIntent.getStringExtra(Intent.EXTRA_TEXT);
+ if (data != null) {
+ mDataString = data;
+ inStream = new ByteArrayInputStream(data.getBytes());
+ }
+ }
+
+ if (inStream == null) {
+ Uri data = mIntent.getData();
+ if (data != null) {
+ mDataUri = data;
+ try {
+ inStream = getContentResolver().openInputStream(data);
+ } catch (FileNotFoundException e) {
+ // didn't work
+ Toast.makeText(this, "failed to open stream", Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+
+ if (inStream == null) {
+ Toast.makeText(this, "no data found", Toast.LENGTH_SHORT).show();
+ finish();
+ return;
+ }
+
+ int contentType = Id.content.unknown;
+ try {
+ contentType = Apg.getStreamContent(this, inStream);
+ inStream.close();
+ } catch (IOException e) {
+ // just means that there's no PGP data in there
+ }
+
+ mList = (ListView) findViewById(R.id.options);
+ Vector<Choice> choices = new Vector<Choice>();
+
+ if (contentType == Id.content.keys) {
+ choices.add(new Choice(Id.choice.action.import_public,
+ getString(R.string.action_importPublic)));
+ choices.add(new Choice(Id.choice.action.import_secret,
+ getString(R.string.action_importSecret)));
+ }
+
+ if (contentType == Id.content.encrypted_data) {
+ choices.add(new Choice(Id.choice.action.decrypt, getString(R.string.action_decrypt)));
+ }
+
+ if (contentType == Id.content.unknown) {
+ choices.add(new Choice(Id.choice.action.encrypt, getString(R.string.action_encrypt)));
+ }
+
+ mAdapter = new ArrayAdapter<Choice>(this, android.R.layout.simple_list_item_1, choices);
+ mList.setAdapter(mAdapter);
+
+ mList.setOnItemClickListener(new OnItemClickListener() {
+ public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
+ clicked(mAdapter.getItem(arg2).getId());
+ }
+ });
+
+ mCancelButton = (Button) findViewById(R.id.btn_cancel);
+ mCancelButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ GeneralActivity.this.finish();
+ }
+ });
+
+ if (choices.size() == 1) {
+ clicked(choices.get(0).getId());
+ }
+ }
+
+ private void clicked(int id) {
+ Intent intent = new Intent();
+ switch (id) {
+ case Id.choice.action.encrypt: {
+ intent.setClass(this, EncryptActivity.class);
+ if (mDataString != null) {
+ intent.setAction(Apg.Intent.ENCRYPT);
+ intent.putExtra(Apg.EXTRA_TEXT, mDataString);
+ } else if (mDataUri != null) {
+ intent.setAction(Apg.Intent.ENCRYPT_FILE);
+ intent.setDataAndType(mDataUri, mIntent.getType());
+ }
+
+ break;
+ }
+
+ case Id.choice.action.decrypt: {
+ intent.setClass(this, DecryptActivity.class);
+ if (mDataString != null) {
+ intent.setAction(Apg.Intent.DECRYPT);
+ intent.putExtra(Apg.EXTRA_TEXT, mDataString);
+ } else if (mDataUri != null) {
+ intent.setAction(Apg.Intent.DECRYPT_FILE);
+ intent.setDataAndType(mDataUri, mIntent.getType());
+ }
+
+ break;
+ }
+
+ case Id.choice.action.import_public: {
+ intent.setClass(this, PublicKeyListActivity.class);
+ intent.setAction(Apg.Intent.IMPORT);
+ if (mDataString != null) {
+ intent.putExtra(Apg.EXTRA_TEXT, mDataString);
+ } else if (mDataUri != null) {
+ intent.setDataAndType(mDataUri, mIntent.getType());
+ }
+ break;
+ }
+
+ case Id.choice.action.import_secret: {
+ intent.setClass(this, SecretKeyListActivity.class);
+ intent.setAction(Apg.Intent.IMPORT);
+ if (mDataString != null) {
+ intent.putExtra(Apg.EXTRA_TEXT, mDataString);
+ } else if (mDataUri != null) {
+ intent.setDataAndType(mDataUri, mIntent.getType());
+ }
+ break;
+ }
+
+ default: {
+ // shouldn't happen
+ return;
+ }
+ }
+
+ startActivity(intent);
+ finish();
+ }
+}
diff --git a/org_apg/src/org/apg/ui/ImportFromQRCodeActivity.java b/org_apg/src/org/apg/ui/ImportFromQRCodeActivity.java
new file mode 100644
index 000000000..593c841df
--- /dev/null
+++ b/org_apg/src/org/apg/ui/ImportFromQRCodeActivity.java
@@ -0,0 +1,138 @@
+package org.apg.ui;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+import org.apg.Apg;
+import org.apg.Constants;
+import org.apg.HkpKeyServer;
+import org.apg.Id;
+import org.apg.KeyServer.QueryException;
+import org.spongycastle.openpgp.PGPKeyRing;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.apg.R;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Message;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.google.zxing.integration.android.IntentIntegrator;
+import com.google.zxing.integration.android.IntentResult;
+
+public class ImportFromQRCodeActivity extends BaseActivity {
+ private static final String TAG = "ImportFromQRCodeActivity";
+
+ private final Bundle status = new Bundle();
+ private final Message msg = new Message();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ new IntentIntegrator(this).initiateScan();
+ }
+
+ private void importAndSign(final long keyId, final String expectedFingerprint) {
+ if (expectedFingerprint != null && expectedFingerprint.length() > 0) {
+
+ Thread t = new Thread() {
+ @Override
+ public void run() {
+ try {
+ // TODO: display some sort of spinner here while the user waits
+
+ HkpKeyServer server = new HkpKeyServer(mPreferences.getKeyServers()[0]); // TODO: there should be only 1
+ String encodedKey = server.get(keyId);
+
+ PGPKeyRing keyring = Apg.decodeKeyRing(new ByteArrayInputStream(encodedKey.getBytes()));
+ if (keyring != null && keyring instanceof PGPPublicKeyRing) {
+ PGPPublicKeyRing publicKeyRing = (PGPPublicKeyRing) keyring;
+
+ // make sure the fingerprints match before we cache this thing
+ String actualFingerprint = Apg.convertToHex(publicKeyRing.getPublicKey().getFingerprint());
+ if (expectedFingerprint.equals(actualFingerprint)) {
+ // store the signed key in our local cache
+ int retval = Apg.storeKeyRingInCache(publicKeyRing);
+ if (retval != Id.return_value.ok && retval != Id.return_value.updated) {
+ status.putString(Apg.EXTRA_ERROR, "Failed to store signed key in local cache");
+ } else {
+ Intent intent = new Intent(ImportFromQRCodeActivity.this, SignKeyActivity.class);
+ intent.putExtra(Apg.EXTRA_KEY_ID, keyId);
+ startActivityForResult(intent, Id.request.sign_key);
+ }
+ } else {
+ status.putString(Apg.EXTRA_ERROR, "Scanned fingerprint does NOT match the fingerprint of the received key. You shouldnt trust this key.");
+ }
+ }
+ } catch (QueryException e) {
+ Log.e(TAG, "Failed to query KeyServer", e);
+ status.putString(Apg.EXTRA_ERROR, "Failed to query KeyServer");
+ status.putInt(Constants.extras.STATUS, Id.message.done);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to query KeyServer", e);
+ status.putString(Apg.EXTRA_ERROR, "Failed to query KeyServer");
+ status.putInt(Constants.extras.STATUS, Id.message.done);
+ }
+ }
+ };
+
+ t.setName("KeyExchange Download Thread");
+ t.setDaemon(true);
+ t.start();
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case IntentIntegrator.REQUEST_CODE: {
+ boolean debug = true; // TODO: remove this!!!
+ IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
+ if (debug || (scanResult != null && scanResult.getFormatName() != null)) {
+ String[] bits = debug ? new String[] { "5993515643896327656", "0816 F68A 6816 68FB 01BF 2CA5 532D 3EB9 1E2F EDE8" } : scanResult.getContents().split(",");
+ if (bits.length != 2) {
+ return; // dont know how to handle this. Not a valid code
+ }
+
+ long keyId = Long.parseLong(bits[0]);
+ String expectedFingerprint = bits[1];
+
+ importAndSign(keyId, expectedFingerprint);
+
+ break;
+ }
+ }
+
+ case Id.request.sign_key: {
+ // signals the end of processing. Signature was either applied, or it wasnt
+ status.putInt(Constants.extras.STATUS, Id.message.done);
+
+ msg.setData(status);
+ sendMessage(msg);
+
+ break;
+ }
+
+ default: {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+ }
+
+ @Override
+ public void doneCallback(Message msg) {
+ super.doneCallback(msg);
+
+ Bundle data = msg.getData();
+ String error = data.getString(Apg.EXTRA_ERROR);
+ if (error != null) {
+ Toast.makeText(this, getString(R.string.errorMessage, error), Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Toast.makeText(this, R.string.keySignSuccess, Toast.LENGTH_SHORT).show(); // TODO
+ finish();
+ }
+}
diff --git a/org_apg/src/org/apg/ui/KeyListActivity.java b/org_apg/src/org/apg/ui/KeyListActivity.java
new file mode 100644
index 000000000..6c76f02bc
--- /dev/null
+++ b/org_apg/src/org/apg/ui/KeyListActivity.java
@@ -0,0 +1,768 @@
+/*
+ * 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.apg.ui;
+
+import org.apg.Apg;
+import org.apg.Constants;
+import org.apg.FileDialog;
+import org.apg.Id;
+import org.apg.InputData;
+import org.apg.provider.KeyRings;
+import org.apg.provider.Keys;
+import org.apg.provider.UserIds;
+import org.spongycastle.openpgp.PGPException;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.apg.R;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.SearchManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Message;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.BaseExpandableListAdapter;
+import android.widget.Button;
+import android.widget.ExpandableListView;
+import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Vector;
+
+public class KeyListActivity extends BaseActivity {
+ protected ExpandableListView mList;
+ protected KeyListAdapter mListAdapter;
+ protected View mFilterLayout;
+ protected Button mClearFilterButton;
+ protected TextView mFilterInfo;
+
+ protected int mSelectedItem = -1;
+ protected int mTask = 0;
+
+ protected String mImportFilename = Constants.path.APP_DIR + "/";
+ protected String mExportFilename = Constants.path.APP_DIR + "/";
+
+ protected String mImportData;
+ protected boolean mDeleteAfterImport = false;
+
+ protected int mKeyType = Id.type.public_key;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.key_list);
+
+ setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
+
+ mList = (ExpandableListView) findViewById(R.id.list);
+ registerForContextMenu(mList);
+
+ mFilterLayout = findViewById(R.id.layout_filter);
+ mFilterInfo = (TextView) mFilterLayout.findViewById(R.id.filterInfo);
+ mClearFilterButton = (Button) mFilterLayout.findViewById(R.id.btn_clear);
+
+ mClearFilterButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ handleIntent(new Intent());
+ }
+ });
+
+ handleIntent(getIntent());
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ handleIntent(intent);
+ }
+
+ protected void handleIntent(Intent intent) {
+ String searchString = null;
+ if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
+ searchString = intent.getStringExtra(SearchManager.QUERY);
+ if (searchString != null && searchString.trim().length() == 0) {
+ searchString = null;
+ }
+ }
+
+ if (searchString == null) {
+ mFilterLayout.setVisibility(View.GONE);
+ } else {
+ mFilterLayout.setVisibility(View.VISIBLE);
+ mFilterInfo.setText(getString(R.string.filterInfo, searchString));
+ }
+
+ if (mListAdapter != null) {
+ mListAdapter.cleanup();
+ }
+ mListAdapter = new KeyListAdapter(this, searchString);
+ mList.setAdapter(mListAdapter);
+
+ if (Apg.Intent.IMPORT.equals(intent.getAction())) {
+ if ("file".equals(intent.getScheme()) && intent.getDataString() != null) {
+ mImportFilename = Uri.decode(intent.getDataString().replace("file://", ""));
+ } else {
+ mImportData = intent.getStringExtra(Apg.EXTRA_TEXT);
+ }
+ importKeys();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case Id.menu.option.import_keys: {
+ showDialog(Id.dialog.import_keys);
+ return true;
+ }
+
+ case Id.menu.option.export_keys: {
+ showDialog(Id.dialog.export_keys);
+ return true;
+ }
+
+ default: {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+ }
+
+ @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 Id.menu.export: {
+ mSelectedItem = groupPosition;
+ showDialog(Id.dialog.export_key);
+ return true;
+ }
+
+ case Id.menu.delete: {
+ mSelectedItem = groupPosition;
+ showDialog(Id.dialog.delete_key);
+ return true;
+ }
+
+ default: {
+ return super.onContextItemSelected(menuItem);
+ }
+ }
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ boolean singleKeyExport = false;
+
+ switch (id) {
+ case Id.dialog.delete_key: {
+ final int keyRingId = mListAdapter.getKeyRingId(mSelectedItem);
+ mSelectedItem = -1;
+ // TODO: better way to do this?
+ String userId = "<unknown>";
+ Object keyRing = Apg.getKeyRing(keyRingId);
+ if (keyRing != null) {
+ if (keyRing instanceof PGPPublicKeyRing) {
+ userId = Apg.getMainUserIdSafe(this,
+ Apg.getMasterKey((PGPPublicKeyRing) keyRing));
+ } else {
+ userId = Apg.getMainUserIdSafe(this,
+ Apg.getMasterKey((PGPSecretKeyRing) keyRing));
+ }
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.warning);
+ builder.setMessage(getString(
+ mKeyType == Id.type.public_key ? R.string.keyDeletionConfirmation
+ : R.string.secretKeyDeletionConfirmation, userId));
+ builder.setIcon(android.R.drawable.ic_dialog_alert);
+ builder.setPositiveButton(R.string.btn_delete, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ deleteKey(keyRingId);
+ removeDialog(Id.dialog.delete_key);
+ }
+ });
+ builder.setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(Id.dialog.delete_key);
+ }
+ });
+ return builder.create();
+ }
+
+ case Id.dialog.import_keys: {
+ return FileDialog.build(this, getString(R.string.title_importKeys),
+ getString(R.string.specifyFileToImportFrom), mImportFilename,
+ new FileDialog.OnClickListener() {
+ public void onOkClick(String filename, boolean checked) {
+ removeDialog(Id.dialog.import_keys);
+ mDeleteAfterImport = checked;
+ mImportFilename = filename;
+ importKeys();
+ }
+
+ public void onCancelClick() {
+ removeDialog(Id.dialog.import_keys);
+ }
+ }, getString(R.string.filemanager_titleOpen),
+ getString(R.string.filemanager_btnOpen),
+ getString(R.string.label_deleteAfterImport), Id.request.filename);
+ }
+
+ case Id.dialog.export_key: {
+ singleKeyExport = true;
+ // break intentionally omitted, to use the Id.dialog.export_keys dialog
+ }
+
+ case Id.dialog.export_keys: {
+ String title = (singleKeyExport ? getString(R.string.title_exportKey)
+ : getString(R.string.title_exportKeys));
+
+ final int thisDialogId = (singleKeyExport ? Id.dialog.export_key
+ : Id.dialog.export_keys);
+
+ return FileDialog.build(this, title,
+ getString(mKeyType == Id.type.public_key ? R.string.specifyFileToExportTo
+ : R.string.specifyFileToExportSecretKeysTo), mExportFilename,
+ new FileDialog.OnClickListener() {
+ public void onOkClick(String filename, boolean checked) {
+ removeDialog(thisDialogId);
+ mExportFilename = filename;
+ exportKeys();
+ }
+
+ public void onCancelClick() {
+ removeDialog(thisDialogId);
+ }
+ }, getString(R.string.filemanager_titleSave),
+ getString(R.string.filemanager_btnSave), null, Id.request.filename);
+ }
+
+ default: {
+ return super.onCreateDialog(id);
+ }
+ }
+ }
+
+ public void importKeys() {
+ showDialog(Id.dialog.importing);
+ mTask = Id.task.import_keys;
+ startThread();
+ }
+
+ public void exportKeys() {
+ showDialog(Id.dialog.exporting);
+ mTask = Id.task.export_keys;
+ startThread();
+ }
+
+ @Override
+ public void run() {
+ String error = null;
+ Bundle data = new Bundle();
+ Message msg = new Message();
+
+ try {
+ InputStream importInputStream = null;
+ OutputStream exportOutputStream = null;
+ long size = 0;
+ if (mTask == Id.task.import_keys) {
+ if (mImportData != null) {
+ byte[] bytes = mImportData.getBytes();
+ size = bytes.length;
+ importInputStream = new ByteArrayInputStream(bytes);
+ } else {
+ File file = new File(mImportFilename);
+ size = file.length();
+ importInputStream = new FileInputStream(file);
+ }
+ } else {
+ exportOutputStream = new FileOutputStream(mExportFilename);
+ }
+
+ if (mTask == Id.task.import_keys) {
+ data = Apg.importKeyRings(this, mKeyType, new InputData(importInputStream, size),
+ this);
+ } else {
+ Vector<Integer> keyRingIds = new Vector<Integer>();
+ if (mSelectedItem == -1) {
+ keyRingIds = Apg
+ .getKeyRingIds(mKeyType == Id.type.public_key ? Id.database.type_public
+ : Id.database.type_secret);
+ } else {
+ int keyRingId = mListAdapter.getKeyRingId(mSelectedItem);
+ keyRingIds.add(keyRingId);
+ mSelectedItem = -1;
+ }
+ data = Apg.exportKeyRings(this, keyRingIds, exportOutputStream, this);
+ }
+ } catch (FileNotFoundException e) {
+ error = getString(R.string.error_fileNotFound);
+ } catch (IOException e) {
+ error = "" + e;
+ } catch (PGPException e) {
+ error = "" + e;
+ } catch (Apg.GeneralException e) {
+ error = "" + e;
+ }
+
+ mImportData = null;
+
+ if (mTask == Id.task.import_keys) {
+ data.putInt(Constants.extras.STATUS, Id.message.import_done);
+ } else {
+ data.putInt(Constants.extras.STATUS, Id.message.export_done);
+ }
+
+ if (error != null) {
+ data.putString(Apg.EXTRA_ERROR, error);
+ }
+
+ msg.setData(data);
+ sendMessage(msg);
+ }
+
+ protected void deleteKey(int keyRingId) {
+ Apg.deleteKey(keyRingId);
+ refreshList();
+ }
+
+ protected void refreshList() {
+ mListAdapter.rebuild(true);
+ mListAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public void doneCallback(Message msg) {
+ super.doneCallback(msg);
+
+ Bundle data = msg.getData();
+ if (data != null) {
+ int type = data.getInt(Constants.extras.STATUS);
+ switch (type) {
+ case Id.message.import_done: {
+ removeDialog(Id.dialog.importing);
+
+ String error = data.getString(Apg.EXTRA_ERROR);
+ if (error != null) {
+ Toast.makeText(KeyListActivity.this, getString(R.string.errorMessage, error),
+ Toast.LENGTH_SHORT).show();
+ } else {
+ int added = data.getInt("added");
+ int updated = data.getInt("updated");
+ int bad = data.getInt("bad");
+ String message;
+ if (added > 0 && updated > 0) {
+ message = getString(R.string.keysAddedAndUpdated, added, updated);
+ } else if (added > 0) {
+ message = getString(R.string.keysAdded, added);
+ } else if (updated > 0) {
+ message = getString(R.string.keysUpdated, updated);
+ } else {
+ message = getString(R.string.noKeysAddedOrUpdated);
+ }
+ Toast.makeText(KeyListActivity.this, message, Toast.LENGTH_SHORT).show();
+ if (bad > 0) {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ alert.setIcon(android.R.drawable.ic_dialog_alert);
+ alert.setTitle(R.string.warning);
+ alert.setMessage(this.getString(R.string.badKeysEncountered, bad));
+
+ alert.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ });
+ alert.setCancelable(true);
+ alert.create().show();
+ } else if (mDeleteAfterImport) {
+ // everything went well, so now delete, if that was turned on
+ setDeleteFile(mImportFilename);
+ showDialog(Id.dialog.delete_file);
+ }
+ }
+ refreshList();
+ break;
+ }
+
+ case Id.message.export_done: {
+ removeDialog(Id.dialog.exporting);
+
+ String error = data.getString(Apg.EXTRA_ERROR);
+ if (error != null) {
+ Toast.makeText(KeyListActivity.this, getString(R.string.errorMessage, error),
+ Toast.LENGTH_SHORT).show();
+ } else {
+ int exported = data.getInt("exported");
+ String message;
+ if (exported == 1) {
+ message = getString(R.string.keyExported);
+ } else if (exported > 0) {
+ message = getString(R.string.keysExported, exported);
+ } else {
+ message = getString(R.string.noKeysExported);
+ }
+ Toast.makeText(KeyListActivity.this, message, Toast.LENGTH_SHORT).show();
+ }
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ }
+ }
+
+ protected class KeyListAdapter extends BaseExpandableListAdapter {
+ private LayoutInflater mInflater;
+ private Vector<Vector<KeyChild>> mChildren;
+ private SQLiteDatabase mDatabase;
+ private Cursor mCursor;
+ private String mSearchString;
+
+ private class KeyChild {
+ public static final int KEY = 0;
+ public static final int USER_ID = 1;
+ public static final int FINGER_PRINT = 2;
+
+ public int type;
+ public String userId;
+ public long keyId;
+ public boolean isMasterKey;
+ public int algorithm;
+ public int keySize;
+ public boolean canSign;
+ public boolean canEncrypt;
+ public String fingerPrint;
+
+ public KeyChild(long keyId, boolean isMasterKey, int algorithm, int keySize,
+ boolean canSign, boolean canEncrypt) {
+ this.type = KEY;
+ this.keyId = keyId;
+ this.isMasterKey = isMasterKey;
+ this.algorithm = algorithm;
+ this.keySize = keySize;
+ this.canSign = canSign;
+ this.canEncrypt = canEncrypt;
+ }
+
+ public KeyChild(String userId) {
+ type = USER_ID;
+ this.userId = userId;
+ }
+
+ public KeyChild(String fingerPrint, boolean isFingerPrint) {
+ type = FINGER_PRINT;
+ this.fingerPrint = fingerPrint;
+ }
+ }
+
+ public KeyListAdapter(Context context, String searchString) {
+ mSearchString = searchString;
+
+ mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mDatabase = Apg.getDatabase().db();
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables(KeyRings.TABLE_NAME + " INNER JOIN " + Keys.TABLE_NAME + " ON " + "("
+ + KeyRings.TABLE_NAME + "." + KeyRings._ID + " = " + Keys.TABLE_NAME + "."
+ + Keys.KEY_RING_ID + " AND " + Keys.TABLE_NAME + "." + Keys.IS_MASTER_KEY
+ + " = '1'" + ") " + " INNER JOIN " + UserIds.TABLE_NAME + " ON " + "("
+ + Keys.TABLE_NAME + "." + Keys._ID + " = " + UserIds.TABLE_NAME + "."
+ + UserIds.KEY_ID + " AND " + UserIds.TABLE_NAME + "." + UserIds.RANK
+ + " = '0')");
+
+ if (searchString != null && searchString.trim().length() > 0) {
+ String[] chunks = searchString.trim().split(" +");
+ qb.appendWhere("EXISTS (SELECT tmp." + UserIds._ID + " FROM " + UserIds.TABLE_NAME
+ + " AS tmp WHERE " + "tmp." + UserIds.KEY_ID + " = " + Keys.TABLE_NAME
+ + "." + Keys._ID);
+ for (int i = 0; i < chunks.length; ++i) {
+ qb.appendWhere(" AND tmp." + UserIds.USER_ID + " LIKE ");
+ qb.appendWhereEscapeString("%" + chunks[i] + "%");
+ }
+ qb.appendWhere(")");
+ }
+
+ mCursor = qb.query(mDatabase, new String[] { KeyRings.TABLE_NAME + "." + KeyRings._ID, // 0
+ KeyRings.TABLE_NAME + "." + KeyRings.MASTER_KEY_ID, // 1
+ UserIds.TABLE_NAME + "." + UserIds.USER_ID, // 2
+ }, KeyRings.TABLE_NAME + "." + KeyRings.TYPE + " = ?", new String[] { ""
+ + (mKeyType == Id.type.public_key ? Id.database.type_public
+ : Id.database.type_secret) }, null, null, UserIds.TABLE_NAME + "."
+ + UserIds.USER_ID + " ASC");
+
+ // content provider way for reference, might have to go back to it sometime:
+ /*
+ * Uri contentUri = null; if (mKeyType == Id.type.secret_key) { contentUri =
+ * Apg.CONTENT_URI_SECRET_KEY_RINGS; } else { contentUri =
+ * Apg.CONTENT_URI_PUBLIC_KEY_RINGS; } mCursor = getContentResolver().query( contentUri,
+ * new String[] { DataProvider._ID, // 0 DataProvider.MASTER_KEY_ID, // 1
+ * DataProvider.USER_ID, // 2 }, null, null, null);
+ */
+
+ startManagingCursor(mCursor);
+ rebuild(false);
+ }
+
+ public void cleanup() {
+ if (mCursor != null) {
+ stopManagingCursor(mCursor);
+ mCursor.close();
+ }
+ }
+
+ public void rebuild(boolean requery) {
+ if (requery) {
+ mCursor.requery();
+ }
+ mChildren = new Vector<Vector<KeyChild>>();
+ for (int i = 0; i < mCursor.getCount(); ++i) {
+ mChildren.add(null);
+ }
+ }
+
+ protected Vector<KeyChild> getChildrenOfGroup(int groupPosition) {
+ Vector<KeyChild> children = mChildren.get(groupPosition);
+ if (children != null) {
+ return children;
+ }
+
+ mCursor.moveToPosition(groupPosition);
+ children = new Vector<KeyChild>();
+ Cursor c = mDatabase.query(Keys.TABLE_NAME, new String[] { Keys._ID, // 0
+ Keys.KEY_ID, // 1
+ Keys.IS_MASTER_KEY, // 2
+ Keys.ALGORITHM, // 3
+ Keys.KEY_SIZE, // 4
+ Keys.CAN_SIGN, // 5
+ Keys.CAN_ENCRYPT, // 6
+ }, Keys.KEY_RING_ID + " = ?", new String[] { mCursor.getString(0) }, null, null,
+ Keys.RANK + " ASC");
+
+ int masterKeyId = -1;
+ long fingerPrintId = -1;
+ for (int i = 0; i < c.getCount(); ++i) {
+ c.moveToPosition(i);
+ children.add(new KeyChild(c.getLong(1), c.getInt(2) == 1, c.getInt(3), c.getInt(4),
+ c.getInt(5) == 1, c.getInt(6) == 1));
+ if (i == 0) {
+ masterKeyId = c.getInt(0);
+ fingerPrintId = c.getLong(1);
+ }
+ }
+ c.close();
+
+ if (masterKeyId != -1) {
+ children.insertElementAt(new KeyChild(Apg.getFingerPrint(fingerPrintId), true), 0);
+ c = mDatabase.query(UserIds.TABLE_NAME, new String[] { UserIds.USER_ID, // 0
+ }, UserIds.KEY_ID + " = ? AND " + UserIds.RANK + " > 0", new String[] { ""
+ + masterKeyId }, null, null, UserIds.RANK + " ASC");
+
+ for (int i = 0; i < c.getCount(); ++i) {
+ c.moveToPosition(i);
+ children.add(new KeyChild(c.getString(0)));
+ }
+ c.close();
+ }
+
+ mChildren.set(groupPosition, children);
+ return children;
+ }
+
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return true;
+ }
+
+ public int getGroupCount() {
+ return mCursor.getCount();
+ }
+
+ public Object getChild(int groupPosition, int childPosition) {
+ return null;
+ }
+
+ public long getChildId(int groupPosition, int childPosition) {
+ return childPosition;
+ }
+
+ public int getChildrenCount(int groupPosition) {
+ return getChildrenOfGroup(groupPosition).size();
+ }
+
+ public Object getGroup(int position) {
+ return position;
+ }
+
+ public long getGroupId(int position) {
+ mCursor.moveToPosition(position);
+ return mCursor.getLong(1); // MASTER_KEY_ID
+ }
+
+ public int getKeyRingId(int position) {
+ mCursor.moveToPosition(position);
+ return mCursor.getInt(0); // _ID
+ }
+
+ public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
+ ViewGroup parent) {
+ mCursor.moveToPosition(groupPosition);
+
+ View 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.mainUserId);
+ mainUserId.setText("");
+ TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest);
+ mainUserIdRest.setText("");
+
+ String userId = mCursor.getString(2); // USER_ID
+ 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.unknownUserId);
+ }
+
+ if (mainUserIdRest.getText().length() == 0) {
+ mainUserIdRest.setVisibility(View.GONE);
+ }
+ return view;
+ }
+
+ public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
+ View convertView, ViewGroup parent) {
+ mCursor.moveToPosition(groupPosition);
+
+ Vector<KeyChild> children = getChildrenOfGroup(groupPosition);
+
+ KeyChild child = children.get(childPosition);
+ View view = null;
+ switch (child.type) {
+ case KeyChild.KEY: {
+ if (child.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.keyId);
+ String keyIdStr = Apg.getSmallFingerPrint(child.keyId);
+ keyId.setText(keyIdStr);
+ TextView keyDetails = (TextView) view.findViewById(R.id.keyDetails);
+ String algorithmStr = Apg.getAlgorithmInfo(child.algorithm, child.keySize);
+ keyDetails.setText("(" + algorithmStr + ")");
+
+ ImageView encryptIcon = (ImageView) view.findViewById(R.id.ic_encryptKey);
+ if (!child.canEncrypt) {
+ encryptIcon.setVisibility(View.GONE);
+ }
+
+ ImageView signIcon = (ImageView) view.findViewById(R.id.ic_signKey);
+ if (!child.canSign) {
+ 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.userId);
+ userId.setText(child.userId);
+ break;
+ }
+
+ case KeyChild.FINGER_PRINT: {
+ view = mInflater.inflate(R.layout.key_list_child_item_user_id, null);
+ TextView userId = (TextView) view.findViewById(R.id.userId);
+ userId.setText(getString(R.string.fingerprint) + ":\n"
+ + child.fingerPrint.replace(" ", "\n"));
+ break;
+ }
+ }
+ return view;
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.request.filename: {
+ if (resultCode == RESULT_OK && data != null) {
+ String filename = data.getDataString();
+ if (filename != null) {
+ // Get rid of URI prefix:
+ if (filename.startsWith("file://")) {
+ filename = filename.substring(7);
+ }
+ // replace %20 and so on
+ filename = Uri.decode(filename);
+
+ FileDialog.setFilename(filename);
+ }
+ }
+ return;
+ }
+
+ default: {
+ break;
+ }
+ }
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+}
diff --git a/org_apg/src/org/apg/ui/KeyServerPreferenceActivity.java b/org_apg/src/org/apg/ui/KeyServerPreferenceActivity.java
new file mode 100644
index 000000000..85d31779a
--- /dev/null
+++ b/org_apg/src/org/apg/ui/KeyServerPreferenceActivity.java
@@ -0,0 +1,125 @@
+/*
+ * 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.apg.ui;
+
+import java.util.Vector;
+
+import org.apg.Apg;
+import org.apg.ui.widget.Editor;
+import org.apg.ui.widget.KeyServerEditor;
+import org.apg.ui.widget.Editor.EditorListener;
+import org.apg.R;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+public class KeyServerPreferenceActivity extends BaseActivity
+ implements OnClickListener, EditorListener {
+ private LayoutInflater mInflater;
+ private ViewGroup mEditors;
+ private View mAdd;
+ private TextView mTitle;
+ private TextView mSummary;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.key_server_preference);
+
+ mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ mTitle = (TextView) findViewById(R.id.title);
+ mSummary = (TextView) findViewById(R.id.summary);
+
+ mTitle.setText(R.string.label_keyServers);
+
+ mEditors = (ViewGroup) findViewById(R.id.editors);
+ mAdd = findViewById(R.id.add);
+ mAdd.setOnClickListener(this);
+
+ Intent intent = getIntent();
+ String servers[] = intent.getStringArrayExtra(Apg.EXTRA_KEY_SERVERS);
+ if (servers != null) {
+ for (int i = 0; i < servers.length; ++i) {
+ KeyServerEditor view = (KeyServerEditor) mInflater.inflate(R.layout.key_server_editor, mEditors, false);
+ view.setEditorListener(this);
+ view.setValue(servers[i]);
+ mEditors.addView(view);
+ }
+ }
+
+ Button okButton = (Button) findViewById(R.id.btn_ok);
+ okButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ okClicked();
+ }
+ });
+
+ Button cancelButton = (Button) findViewById(R.id.btn_cancel);
+ cancelButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ cancelClicked();
+ }
+ });
+ }
+
+ public void onDeleted(Editor editor) {
+ // nothing to do
+ }
+
+ public void onClick(View v) {
+ KeyServerEditor view = (KeyServerEditor) mInflater.inflate(R.layout.key_server_editor, mEditors, false);
+ view.setEditorListener(this);
+ mEditors.addView(view);
+ }
+
+ private void cancelClicked() {
+ setResult(RESULT_CANCELED, null);
+ finish();
+ }
+
+ private void okClicked() {
+ Intent data = new Intent();
+ Vector<String> servers = new Vector<String>();
+ for (int i = 0; i < mEditors.getChildCount(); ++i) {
+ KeyServerEditor editor = (KeyServerEditor) mEditors.getChildAt(i);
+ String tmp = editor.getValue();
+ if (tmp.length() > 0) {
+ servers.add(tmp);
+ }
+ }
+ String[] dummy = new String[0];
+ data.putExtra(Apg.EXTRA_KEY_SERVERS, servers.toArray(dummy));
+ setResult(RESULT_OK, data);
+ finish();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // override this, so no option menu is added (as would be in BaseActivity), since
+ // we're still in preferences
+ return true;
+ }
+}
diff --git a/org_apg/src/org/apg/ui/KeyServerQueryActivity.java b/org_apg/src/org/apg/ui/KeyServerQueryActivity.java
new file mode 100644
index 000000000..606acb575
--- /dev/null
+++ b/org_apg/src/org/apg/ui/KeyServerQueryActivity.java
@@ -0,0 +1,297 @@
+package org.apg.ui;
+
+import java.util.List;
+import java.util.Vector;
+
+import org.apg.Apg;
+import org.apg.Constants;
+import org.apg.HkpKeyServer;
+import org.apg.Id;
+import org.apg.KeyServer.InsufficientQuery;
+import org.apg.KeyServer.KeyInfo;
+import org.apg.KeyServer.QueryException;
+import org.apg.KeyServer.TooManyResponses;
+import org.apg.R;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Message;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.LinearLayout.LayoutParams;
+import android.widget.ListView;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class KeyServerQueryActivity extends BaseActivity {
+ private ListView mList;
+ private EditText mQuery;
+ private Button mSearch;
+ private KeyInfoListAdapter mAdapter;
+ private Spinner mKeyServer;
+
+ private int mQueryType;
+ private String mQueryString;
+ private long mQueryId;
+ private volatile List<KeyInfo> mSearchResult;
+ private volatile String mKeyData;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.key_server_query_layout);
+
+ mQuery = (EditText) findViewById(R.id.query);
+ mSearch = (Button) findViewById(R.id.btn_search);
+ mList = (ListView) findViewById(R.id.list);
+ mAdapter = new KeyInfoListAdapter(this);
+ mList.setAdapter(mAdapter);
+
+ mKeyServer = (Spinner) findViewById(R.id.keyServer);
+ ArrayAdapter<String> adapter =
+ new ArrayAdapter<String>(this,
+ android.R.layout.simple_spinner_item,
+ mPreferences.getKeyServers());
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mKeyServer.setAdapter(adapter);
+ if (adapter.getCount() > 0) {
+ mKeyServer.setSelection(0);
+ } else {
+ mSearch.setEnabled(false);
+ }
+
+ mList.setOnItemClickListener(new OnItemClickListener() {
+ public void onItemClick(AdapterView<?> adapter, View view, int position, long keyId) {
+ get(keyId);
+ }
+ });
+
+ mSearch.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ String query = mQuery.getText().toString();
+ search(query);
+ }
+ });
+
+ Intent intent = getIntent();
+ if (Apg.Intent.LOOK_UP_KEY_ID.equals(intent.getAction()) ||
+ Apg.Intent.LOOK_UP_KEY_ID_AND_RETURN.equals(intent.getAction())) {
+ long keyId = intent.getLongExtra(Apg.EXTRA_KEY_ID, 0);
+ if (keyId != 0) {
+ String query = "0x" + Apg.keyToHex(keyId);
+ mQuery.setText(query);
+ search(query);
+ }
+ }
+ }
+
+ private void search(String query) {
+ showDialog(Id.dialog.querying);
+ mQueryType = Id.keyserver.search;
+ mQueryString = query;
+ mAdapter.setKeys(new Vector<KeyInfo>());
+ startThread();
+ }
+
+ private void get(long keyId) {
+ showDialog(Id.dialog.querying);
+ mQueryType = Id.keyserver.get;
+ mQueryId = keyId;
+ startThread();
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ ProgressDialog progress = (ProgressDialog) super.onCreateDialog(id);
+ progress.setMessage(this.getString(R.string.progress_queryingServer,
+ (String)mKeyServer.getSelectedItem()));
+ return progress;
+ }
+
+ @Override
+ public void run() {
+ String error = null;
+ Bundle data = new Bundle();
+ Message msg = new Message();
+
+ try {
+ HkpKeyServer server = new HkpKeyServer((String)mKeyServer.getSelectedItem());
+ if (mQueryType == Id.keyserver.search) {
+ mSearchResult = server.search(mQueryString);
+ } else if (mQueryType == Id.keyserver.get) {
+ mKeyData = server.get(mQueryId);
+ }
+ } catch (QueryException e) {
+ error = "" + e;
+ } catch (InsufficientQuery e) {
+ error = "Insufficient query.";
+ } catch (TooManyResponses e) {
+ error = "Too many responses.";
+ }
+
+ data.putInt(Constants.extras.STATUS, Id.message.done);
+
+ if (error != null) {
+ data.putString(Apg.EXTRA_ERROR, error);
+ }
+
+ msg.setData(data);
+ sendMessage(msg);
+ }
+
+ @Override
+ public void doneCallback(Message msg) {
+ super.doneCallback(msg);
+
+ removeDialog(Id.dialog.querying);
+
+ Bundle data = msg.getData();
+ String error = data.getString(Apg.EXTRA_ERROR);
+ if (error != null) {
+ Toast.makeText(this, getString(R.string.errorMessage, error), Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (mQueryType == Id.keyserver.search) {
+ if (mSearchResult != null) {
+ Toast.makeText(this, getString(R.string.keysFound, mSearchResult.size()), Toast.LENGTH_SHORT).show();
+ mAdapter.setKeys(mSearchResult);
+ }
+ } else if (mQueryType == Id.keyserver.get) {
+ Intent orgIntent = getIntent();
+ if (Apg.Intent.LOOK_UP_KEY_ID_AND_RETURN.equals(orgIntent.getAction())) {
+ if (mKeyData != null) {
+ Intent intent = new Intent();
+ intent.putExtra(Apg.EXTRA_TEXT, mKeyData);
+ setResult(RESULT_OK, intent);
+ } else {
+ setResult(RESULT_CANCELED);
+ }
+ finish();
+ } else {
+ if (mKeyData != null) {
+ Intent intent = new Intent(this, PublicKeyListActivity.class);
+ intent.setAction(Apg.Intent.IMPORT);
+ intent.putExtra(Apg.EXTRA_TEXT, mKeyData);
+ startActivity(intent);
+ }
+ }
+ }
+ }
+
+ public class KeyInfoListAdapter extends BaseAdapter {
+ protected LayoutInflater mInflater;
+ protected Activity mActivity;
+ protected List<KeyInfo> mKeys;
+
+ public KeyInfoListAdapter(Activity activity) {
+ mActivity = activity;
+ mInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mKeys = new Vector<KeyInfo>();
+ }
+
+ public void setKeys(List<KeyInfo> keys) {
+ mKeys = keys;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ public int getCount() {
+ return mKeys.size();
+ }
+
+ public Object getItem(int position) {
+ return mKeys.get(position);
+ }
+
+ public long getItemId(int position) {
+ return mKeys.get(position).keyId;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ KeyInfo keyInfo = mKeys.get(position);
+
+ View view = mInflater.inflate(R.layout.key_server_query_result_item, null);
+
+ TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId);
+ mainUserId.setText(R.string.unknownUserId);
+ TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest);
+ mainUserIdRest.setText("");
+ TextView keyId = (TextView) view.findViewById(R.id.keyId);
+ keyId.setText(R.string.noKey);
+ TextView algorithm = (TextView) view.findViewById(R.id.algorithm);
+ algorithm.setText("");
+ TextView status = (TextView) view.findViewById(R.id.status);
+ status.setText("");
+
+ String userId = keyInfo.userIds.get(0);
+ if (userId != null) {
+ String chunks[] = userId.split(" <", 2);
+ userId = chunks[0];
+ if (chunks.length > 1) {
+ mainUserIdRest.setText("<" + chunks[1]);
+ }
+ mainUserId.setText(userId);
+ }
+
+ keyId.setText(Apg.getSmallFingerPrint(keyInfo.keyId));
+
+ if (mainUserIdRest.getText().length() == 0) {
+ mainUserIdRest.setVisibility(View.GONE);
+ }
+
+ algorithm.setText("" + keyInfo.size + "/" + keyInfo.algorithm);
+
+ if (keyInfo.revoked != null) {
+ status.setText("revoked");
+ } else {
+ status.setVisibility(View.GONE);
+ }
+
+ LinearLayout ll = (LinearLayout) view.findViewById(R.id.list);
+ if (keyInfo.userIds.size() == 1) {
+ ll.setVisibility(View.GONE);
+ } else {
+ boolean first = true;
+ boolean second = true;
+ for (String uid : keyInfo.userIds) {
+ if (first) {
+ first = false;
+ continue;
+ }
+ if (!second) {
+ View sep = new View(mActivity);
+ sep.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, 1));
+ sep.setBackgroundResource(android.R.drawable.divider_horizontal_dark);
+ ll.addView(sep);
+ }
+ TextView uidView = (TextView) mInflater.inflate(R.layout.key_server_query_result_user_id, null);
+ uidView.setText(uid);
+ ll.addView(uidView);
+ second = false;
+ }
+ }
+
+ return view;
+ }
+ }
+}
diff --git a/org_apg/src/org/apg/ui/MailListActivity.java b/org_apg/src/org/apg/ui/MailListActivity.java
new file mode 100644
index 000000000..ad1d08068
--- /dev/null
+++ b/org_apg/src/org/apg/ui/MailListActivity.java
@@ -0,0 +1,222 @@
+/*
+ * 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.apg.ui;
+
+import java.util.Vector;
+import java.util.regex.Matcher;
+
+import org.apg.Apg;
+import org.apg.Preferences;
+import org.apg.R;
+
+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.AdapterView.OnItemClickListener;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+
+public class MailListActivity extends ListActivity {
+ LayoutInflater mInflater = null;
+
+ public static final String EXTRA_ACCOUNT = "account";
+
+ private static 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 static class Message {
+ public Conversation parent;
+ public long id;
+ public String subject;
+ public String fromAddress;
+ public String data;
+ public String replyTo;
+ public boolean signedOnly;
+
+ public Message(Conversation parent, long id, String subject,
+ String fromAddress, String replyTo,
+ String data, boolean signedOnly) {
+ 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;
+ }
+ this.signedOnly = signedOnly;
+ }
+ }
+
+ private Vector<Conversation> mConversations;
+ private Vector<Message> mMessages;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ Preferences prefs = Preferences.getPreferences(this);
+ BaseActivity.setLanguage(this, prefs.getLanguage());
+
+ super.onCreate(savedInstanceState);
+
+ mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ mConversations = new Vector<Conversation>();
+ mMessages = new Vector<Message>();
+
+ String account = getIntent().getExtras().getString(EXTRA_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();
+ boolean signedOnly = false;
+ Matcher matcher = Apg.PGP_MESSAGE.matcher(data);
+ if (matcher.matches()) {
+ data = matcher.group(1);
+ } else {
+ matcher = Apg.PGP_SIGNED_MESSAGE.matcher(data);
+ if (matcher.matches()) {
+ data = matcher.group(1);
+ signedOnly = true;
+ } else {
+ data = null;
+ }
+ }
+ Message message =
+ new Message(conversation,
+ messageCursor.getLong(idIndex),
+ messageCursor.getString(subjectIndex),
+ messageCursor.getString(fromAddressIndex),
+ messageCursor.getString(replyToIndex),
+ data, signedOnly);
+
+ messages.add(message);
+ mMessages.add(message);
+ }
+ conversation.messages = messages;
+ mConversations.add(conversation);
+ }
+
+ setListAdapter(new MailboxAdapter());
+ getListView().setOnItemClickListener(new OnItemClickListener() {
+ public void onItemClick(AdapterView<?> arg0, View v, int position, long id) {
+ Intent intent = new Intent(MailListActivity.this, DecryptActivity.class);
+ intent.setAction(Apg.Intent.DECRYPT);
+ Message message = (Message) ((MailboxAdapter) getListAdapter()).getItem(position);
+ intent.putExtra(Apg.EXTRA_TEXT, message.data);
+ intent.putExtra(Apg.EXTRA_SUBJECT, message.subject);
+ intent.putExtra(Apg.EXTRA_REPLY_TO, 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;
+ }
+
+ public int getCount() {
+ return mMessages.size();
+ }
+
+ public Object getItem(int position) {
+ return mMessages.get(position);
+ }
+
+ public long getItemId(int position) {
+ return mMessages.get(position).id;
+ }
+
+ 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.emailAddress);
+ ImageView status = (ImageView) view.findViewById(R.id.ic_status);
+
+ subject.setText(message.subject);
+ email.setText(message.fromAddress);
+ if (message.data != null) {
+ if (message.signedOnly) {
+ status.setImageResource(R.drawable.signed);
+ } else {
+ status.setImageResource(R.drawable.encrypted);
+ }
+ status.setVisibility(View.VISIBLE);
+ } else {
+ status.setVisibility(View.INVISIBLE);
+ }
+
+ return view;
+ }
+ }
+}
diff --git a/org_apg/src/org/apg/ui/MainActivity.java b/org_apg/src/org/apg/ui/MainActivity.java
new file mode 100644
index 000000000..8c985c2ac
--- /dev/null
+++ b/org_apg/src/org/apg/ui/MainActivity.java
@@ -0,0 +1,419 @@
+/*
+ * 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.apg.ui;
+
+import java.security.Security;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apg.Apg;
+import org.apg.Id;
+import org.apg.Id.dialog;
+import org.apg.Id.menu;
+import org.apg.Id.menu.option;
+import org.apg.provider.Accounts;
+import org.spongycastle.jce.provider.BouncyCastleProvider;
+import org.apg.R;
+
+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.database.Cursor;
+import android.database.SQLException;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.util.Linkify;
+import android.text.util.Linkify.TransformFilter;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+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;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.Button;
+import android.widget.CursorAdapter;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class MainActivity extends BaseActivity {
+ static {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
+ private ListView mAccounts = null;
+ private AccountListAdapter mListAdapter = null;
+ private Cursor mAccountCursor;
+
+ @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);
+ Button encryptFileButton = (Button) findViewById(R.id.btn_encryptFile);
+ Button decryptFileButton = (Button) findViewById(R.id.btn_decryptFile);
+ mAccounts = (ListView) findViewById(R.id.accounts);
+
+ encryptMessageButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ Intent intent = new Intent(MainActivity.this, EncryptActivity.class);
+ intent.setAction(Apg.Intent.ENCRYPT);
+ startActivity(intent);
+ }
+ });
+
+ decryptMessageButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ Intent intent = new Intent(MainActivity.this, DecryptActivity.class);
+ intent.setAction(Apg.Intent.DECRYPT);
+ startActivity(intent);
+ }
+ });
+
+ encryptFileButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ Intent intent = new Intent(MainActivity.this, EncryptActivity.class);
+ intent.setAction(Apg.Intent.ENCRYPT_FILE);
+ startActivity(intent);
+ }
+ });
+
+ decryptFileButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ Intent intent = new Intent(MainActivity.this, DecryptActivity.class);
+ intent.setAction(Apg.Intent.DECRYPT_FILE);
+ startActivity(intent);
+ }
+ });
+
+ mAccountCursor =
+ Apg.getDatabase().db().query(Accounts.TABLE_NAME,
+ new String[] {
+ Accounts._ID,
+ Accounts.NAME,
+ }, null, null, null, null, Accounts.NAME + " ASC");
+ startManagingCursor(mAccountCursor);
+
+ mListAdapter = new AccountListAdapter(this, mAccountCursor);
+ mAccounts.setAdapter(mListAdapter);
+ mAccounts.setOnItemClickListener(new OnItemClickListener() {
+ public void onItemClick(AdapterView<?> arg0, View view, int index, long id) {
+ String accountName = (String) mAccounts.getItemAtPosition(index);
+ startActivity(new Intent(MainActivity.this, MailListActivity.class)
+ .putExtra(MailListActivity.EXTRA_ACCOUNT, accountName));
+ }
+ });
+ registerForContextMenu(mAccounts);
+
+ if (!mPreferences.hasSeenHelp()) {
+ showDialog(Id.dialog.help);
+ }
+
+ if (Apg.isReleaseVersion(this) && !mPreferences.hasSeenChangeLog(Apg.getVersion(this))) {
+ showDialog(Id.dialog.change_log);
+ }
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case Id.dialog.new_account: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ alert.setTitle(R.string.title_addAccount);
+ alert.setMessage(R.string.specifyGoogleMailAccount);
+
+ LayoutInflater inflater =
+ (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.add_account_dialog, null);
+
+ final EditText input = (EditText) view.findViewById(R.id.input);
+ alert.setView(view);
+
+ alert.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ MainActivity.this.removeDialog(Id.dialog.new_account);
+ String accountName = "" + input.getText();
+
+ try {
+ Cursor testCursor =
+ managedQuery(Uri.parse("content://gmail-ls/conversations/" +
+ accountName),
+ null, null, null, null);
+ if (testCursor == null) {
+ Toast.makeText(MainActivity.this,
+ getString(R.string.errorMessage,
+ getString(R.string.error_accountNotFound,
+ accountName)),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ } catch (SecurityException e) {
+ Toast.makeText(MainActivity.this,
+ getString(R.string.errorMessage,
+ getString(R.string.error_accountReadingNotAllowed)),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ ContentValues values = new ContentValues();
+ values.put(Accounts.NAME, accountName);
+ try {
+ Apg.getDatabase().db().insert(Accounts.TABLE_NAME,
+ Accounts.NAME, values);
+ mAccountCursor.requery();
+ mListAdapter.notifyDataSetChanged();
+ } catch (SQLException e) {
+ Toast.makeText(MainActivity.this,
+ getString(R.string.errorMessage,
+ getString(R.string.error_addingAccountFailed,
+ accountName)),
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+
+ alert.setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ MainActivity.this.removeDialog(Id.dialog.new_account);
+ }
+ });
+
+ return alert.create();
+ }
+
+ case Id.dialog.change_log: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ alert.setTitle("Changes " + Apg.getFullVersion(this));
+ LayoutInflater inflater =
+ (LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View layout = inflater.inflate(R.layout.info, null);
+ TextView message = (TextView) layout.findViewById(R.id.message);
+
+ message.setText("Changes:\n" +
+ "* \n" +
+ "\n" +
+ "WARNING: be careful editing your existing keys, as they " +
+ "WILL be stripped of certificates right now.\n" +
+ "\n" +
+ "Also: 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.");
+ alert.setView(layout);
+
+ alert.setCancelable(false);
+ alert.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ MainActivity.this.removeDialog(Id.dialog.change_log);
+ mPreferences.setHasSeenChangeLog(
+ Apg.getVersion(MainActivity.this), true);
+ }
+ });
+
+ return alert.create();
+ }
+
+ case Id.dialog.help: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ alert.setTitle(R.string.title_help);
+
+ LayoutInflater inflater =
+ (LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View layout = inflater.inflate(R.layout.info, null);
+ TextView message = (TextView) layout.findViewById(R.id.message);
+ message.setText(R.string.text_help);
+
+ TransformFilter packageNames = new TransformFilter() {
+ public final String transformUrl(final Matcher match, String url) {
+ String name = match.group(1).toLowerCase();
+ if (name.equals("astro")) {
+ return "com.metago.astro";
+ } else if (name.equals("k-9 mail")) {
+ return "com.fsck.k9";
+ } else {
+ return "org.openintents.filemanager";
+ }
+ }
+ };
+
+ Pattern pattern = Pattern.compile("(OI File Manager|ASTRO|K-9 Mail)");
+ String scheme = "market://search?q=pname:";
+ message.setAutoLinkMask(0);
+ Linkify.addLinks(message, pattern, scheme, null, packageNames);
+
+ alert.setView(layout);
+
+ alert.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ MainActivity.this.removeDialog(Id.dialog.help);
+ mPreferences.setHasSeenHelp(true);
+ }
+ });
+
+ return alert.create();
+ }
+
+ default: {
+ return super.onCreateDialog(id);
+ }
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, Id.menu.option.manage_public_keys, 0, R.string.menu_managePublicKeys)
+ .setIcon(android.R.drawable.ic_menu_manage);
+ menu.add(0, Id.menu.option.manage_secret_keys, 1, R.string.menu_manageSecretKeys)
+ .setIcon(android.R.drawable.ic_menu_manage);
+ menu.add(1, Id.menu.option.create, 2, R.string.menu_addAccount)
+ .setIcon(android.R.drawable.ic_menu_add);
+ menu.add(2, Id.menu.option.preferences, 3, R.string.menu_preferences)
+ .setIcon(android.R.drawable.ic_menu_preferences);
+ menu.add(2, Id.menu.option.key_server, 4, R.string.menu_keyServer)
+ .setIcon(android.R.drawable.ic_menu_search);
+ menu.add(3, Id.menu.option.about, 5, R.string.menu_about)
+ .setIcon(android.R.drawable.ic_menu_info_details);
+ menu.add(3, Id.menu.option.help, 6, R.string.menu_help)
+ .setIcon(android.R.drawable.ic_menu_help);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case Id.menu.option.create: {
+ showDialog(Id.dialog.new_account);
+ return true;
+ }
+
+ case Id.menu.option.manage_public_keys: {
+ startActivity(new Intent(this, PublicKeyListActivity.class));
+ return true;
+ }
+
+ case Id.menu.option.manage_secret_keys: {
+ startActivity(new Intent(this, SecretKeyListActivity.class));
+ return true;
+ }
+
+ case Id.menu.option.help: {
+ showDialog(Id.dialog.help);
+ return true;
+ }
+
+ case Id.menu.option.key_server: {
+ startActivity(new Intent(this, KeyServerQueryActivity.class));
+ return true;
+ }
+
+ default: {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+
+ TextView nameTextView = (TextView) v.findViewById(R.id.accountName);
+ if (nameTextView != null) {
+ menu.setHeaderTitle(nameTextView.getText());
+ menu.add(0, Id.menu.delete, 0, R.string.menu_deleteAccount);
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ AdapterView.AdapterContextMenuInfo info =
+ (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+
+ switch (menuItem.getItemId()) {
+ case Id.menu.delete: {
+ Apg.getDatabase().db().delete(Accounts.TABLE_NAME,
+ Accounts._ID + " = ?",
+ new String[] { "" + info.id });
+ mAccountCursor.requery();
+ mListAdapter.notifyDataSetChanged();
+ return true;
+ }
+
+ default: {
+ return super.onContextItemSelected(menuItem);
+ }
+ }
+ }
+
+
+ private static 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 Object getItem(int position) {
+ Cursor c = getCursor();
+ c.moveToPosition(position);
+ return c.getString(c.getColumnIndex(Accounts.NAME));
+ }
+
+ @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.accountName);
+ int nameIndex = cursor.getColumnIndex(Accounts.NAME);
+ final String account = cursor.getString(nameIndex);
+ nameTextView.setText(account);
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return true;
+ }
+ }
+} \ No newline at end of file
diff --git a/org_apg/src/org/apg/ui/PreferencesActivity.java b/org_apg/src/org/apg/ui/PreferencesActivity.java
new file mode 100644
index 000000000..421c9cc39
--- /dev/null
+++ b/org_apg/src/org/apg/ui/PreferencesActivity.java
@@ -0,0 +1,274 @@
+/*
+ * 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.apg.ui;
+
+import org.apg.Apg;
+import org.apg.Constants;
+import org.apg.Id;
+import org.apg.Preferences;
+import org.apg.Constants.pref;
+import org.apg.Id.choice;
+import org.apg.Id.request;
+import org.apg.Id.choice.compression;
+import org.apg.ui.widget.IntegerListPreference;
+import org.spongycastle.bcpg.HashAlgorithmTags;
+import org.spongycastle.openpgp.PGPEncryptedData;
+import org.apg.R;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceScreen;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Vector;
+
+public class PreferencesActivity extends PreferenceActivity {
+ private ListPreference mLanguage = null;
+ private IntegerListPreference mPassPhraseCacheTtl = null;
+ private IntegerListPreference mEncryptionAlgorithm = null;
+ private IntegerListPreference mHashAlgorithm = null;
+ private IntegerListPreference mMessageCompression = null;
+ private IntegerListPreference mFileCompression = null;
+ private CheckBoxPreference mAsciiArmour = null;
+ private CheckBoxPreference mForceV3Signatures = null;
+ private PreferenceScreen mKeyServerPreference = null;
+ private Preferences mPreferences;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ mPreferences = Preferences.getPreferences(this);
+ BaseActivity.setLanguage(this, mPreferences.getLanguage());
+ super.onCreate(savedInstanceState);
+
+ addPreferencesFromResource(R.xml.apg_preferences);
+
+ mLanguage = (ListPreference) findPreference(Constants.pref.LANGUAGE);
+ Vector<CharSequence> entryVector = new Vector<CharSequence>(Arrays.asList(mLanguage.getEntries()));
+ Vector<CharSequence> entryValueVector = new Vector<CharSequence>(Arrays.asList(mLanguage.getEntryValues()));
+ String supportedLanguages[] = getResources().getStringArray(R.array.supported_languages);
+ HashSet<String> supportedLanguageSet = new HashSet<String>(Arrays.asList(supportedLanguages));
+ for (int i = entryVector.size() - 1; i > -1; --i)
+ {
+ if (!supportedLanguageSet.contains(entryValueVector.get(i)))
+ {
+ entryVector.remove(i);
+ entryValueVector.remove(i);
+ }
+ }
+ CharSequence dummy[] = new CharSequence[0];
+ mLanguage.setEntries(entryVector.toArray(dummy));
+ mLanguage.setEntryValues(entryValueVector.toArray(dummy));
+ mLanguage.setValue(mPreferences.getLanguage());
+ mLanguage.setSummary(mLanguage.getEntry());
+ mLanguage.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener()
+ {
+ public boolean onPreferenceChange(Preference preference, Object newValue)
+ {
+ mLanguage.setValue(newValue.toString());
+ mLanguage.setSummary(mLanguage.getEntry());
+ mPreferences.setLanguage(newValue.toString());
+ return false;
+ }
+ });
+
+ mPassPhraseCacheTtl = (IntegerListPreference) findPreference(Constants.pref.PASS_PHRASE_CACHE_TTL);
+ mPassPhraseCacheTtl.setValue("" + mPreferences.getPassPhraseCacheTtl());
+ mPassPhraseCacheTtl.setSummary(mPassPhraseCacheTtl.getEntry());
+ mPassPhraseCacheTtl.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener()
+ {
+ public boolean onPreferenceChange(Preference preference, Object newValue)
+ {
+ mPassPhraseCacheTtl.setValue(newValue.toString());
+ mPassPhraseCacheTtl.setSummary(mPassPhraseCacheTtl.getEntry());
+ mPreferences.setPassPhraseCacheTtl(Integer.parseInt(newValue.toString()));
+ BaseActivity.startCacheService(PreferencesActivity.this, mPreferences);
+ return false;
+ }
+ });
+
+ mEncryptionAlgorithm = (IntegerListPreference) findPreference(Constants.pref.DEFAULT_ENCRYPTION_ALGORITHM);
+ int valueIds[] = {
+ PGPEncryptedData.AES_128, PGPEncryptedData.AES_192, PGPEncryptedData.AES_256,
+ PGPEncryptedData.BLOWFISH, PGPEncryptedData.TWOFISH, PGPEncryptedData.CAST5,
+ PGPEncryptedData.DES, PGPEncryptedData.TRIPLE_DES, PGPEncryptedData.IDEA,
+ };
+ String entries[] = {
+ "AES-128", "AES-192", "AES-256",
+ "Blowfish", "Twofish", "CAST5",
+ "DES", "Triple DES", "IDEA",
+ };
+ String values[] = new String[valueIds.length];
+ for (int i = 0; i < values.length; ++i) {
+ values[i] = "" + valueIds[i];
+ }
+ mEncryptionAlgorithm.setEntries(entries);
+ mEncryptionAlgorithm.setEntryValues(values);
+ mEncryptionAlgorithm.setValue("" + mPreferences.getDefaultEncryptionAlgorithm());
+ mEncryptionAlgorithm.setSummary(mEncryptionAlgorithm.getEntry());
+ mEncryptionAlgorithm.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener()
+ {
+ public boolean onPreferenceChange(Preference preference, Object newValue)
+ {
+ mEncryptionAlgorithm.setValue(newValue.toString());
+ mEncryptionAlgorithm.setSummary(mEncryptionAlgorithm.getEntry());
+ mPreferences.setDefaultEncryptionAlgorithm(Integer.parseInt(newValue.toString()));
+ return false;
+ }
+ });
+
+ mHashAlgorithm = (IntegerListPreference) findPreference(Constants.pref.DEFAULT_HASH_ALGORITHM);
+ valueIds = new int[] {
+ HashAlgorithmTags.MD5, HashAlgorithmTags.RIPEMD160, HashAlgorithmTags.SHA1,
+ HashAlgorithmTags.SHA224, HashAlgorithmTags.SHA256, HashAlgorithmTags.SHA384,
+ HashAlgorithmTags.SHA512,
+ };
+ entries = new String[] {
+ "MD5", "RIPEMD-160", "SHA-1",
+ "SHA-224", "SHA-256", "SHA-384",
+ "SHA-512",
+ };
+ values = new String[valueIds.length];
+ for (int i = 0; i < values.length; ++i) {
+ values[i] = "" + valueIds[i];
+ }
+ mHashAlgorithm.setEntries(entries);
+ mHashAlgorithm.setEntryValues(values);
+ mHashAlgorithm.setValue("" + mPreferences.getDefaultHashAlgorithm());
+ mHashAlgorithm.setSummary(mHashAlgorithm.getEntry());
+ mHashAlgorithm.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener()
+ {
+ public boolean onPreferenceChange(Preference preference, Object newValue)
+ {
+ mHashAlgorithm.setValue(newValue.toString());
+ mHashAlgorithm.setSummary(mHashAlgorithm.getEntry());
+ mPreferences.setDefaultHashAlgorithm(Integer.parseInt(newValue.toString()));
+ return false;
+ }
+ });
+
+ mMessageCompression = (IntegerListPreference) findPreference(Constants.pref.DEFAULT_MESSAGE_COMPRESSION);
+ valueIds = new int[] {
+ Id.choice.compression.none,
+ Id.choice.compression.zip,
+ Id.choice.compression.zlib,
+ Id.choice.compression.bzip2,
+ };
+ entries = new String[] {
+ getString(R.string.choice_none) + " (" + getString(R.string.fast) + ")",
+ "ZIP (" + getString(R.string.fast) + ")",
+ "ZLIB (" + getString(R.string.fast) + ")",
+ "BZIP2 (" + getString(R.string.very_slow) + ")",
+ };
+ values = new String[valueIds.length];
+ for (int i = 0; i < values.length; ++i) {
+ values[i] = "" + valueIds[i];
+ }
+ mMessageCompression.setEntries(entries);
+ mMessageCompression.setEntryValues(values);
+ mMessageCompression.setValue("" + mPreferences.getDefaultMessageCompression());
+ mMessageCompression.setSummary(mMessageCompression.getEntry());
+ mMessageCompression.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener()
+ {
+ public boolean onPreferenceChange(Preference preference, Object newValue)
+ {
+ mMessageCompression.setValue(newValue.toString());
+ mMessageCompression.setSummary(mMessageCompression.getEntry());
+ mPreferences.setDefaultMessageCompression(Integer.parseInt(newValue.toString()));
+ return false;
+ }
+ });
+
+ mFileCompression = (IntegerListPreference) findPreference(Constants.pref.DEFAULT_FILE_COMPRESSION);
+ mFileCompression.setEntries(entries);
+ mFileCompression.setEntryValues(values);
+ mFileCompression.setValue("" + mPreferences.getDefaultFileCompression());
+ mFileCompression.setSummary(mFileCompression.getEntry());
+ mFileCompression.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener()
+ {
+ public boolean onPreferenceChange(Preference preference, Object newValue)
+ {
+ mFileCompression.setValue(newValue.toString());
+ mFileCompression.setSummary(mFileCompression.getEntry());
+ mPreferences.setDefaultFileCompression(Integer.parseInt(newValue.toString()));
+ return false;
+ }
+ });
+
+ mAsciiArmour = (CheckBoxPreference) findPreference(Constants.pref.DEFAULT_ASCII_ARMOUR);
+ mAsciiArmour.setChecked(mPreferences.getDefaultAsciiArmour());
+ mAsciiArmour.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener()
+ {
+ public boolean onPreferenceChange(Preference preference, Object newValue)
+ {
+ mAsciiArmour.setChecked((Boolean)newValue);
+ mPreferences.setDefaultAsciiArmour((Boolean)newValue);
+ return false;
+ }
+ });
+
+ mForceV3Signatures = (CheckBoxPreference) findPreference(Constants.pref.FORCE_V3_SIGNATURES);
+ mForceV3Signatures.setChecked(mPreferences.getForceV3Signatures());
+ mForceV3Signatures.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener()
+ {
+ public boolean onPreferenceChange(Preference preference, Object newValue)
+ {
+ mForceV3Signatures.setChecked((Boolean)newValue);
+ mPreferences.setForceV3Signatures((Boolean)newValue);
+ return false;
+ }
+ });
+
+ mKeyServerPreference = (PreferenceScreen) findPreference(Constants.pref.KEY_SERVERS);
+ String servers[] = mPreferences.getKeyServers();
+ mKeyServerPreference.setSummary(getResources().getString(R.string.nKeyServers, servers.length));
+ mKeyServerPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ public boolean onPreferenceClick(Preference preference) {
+ Intent intent = new Intent(PreferencesActivity.this,
+ KeyServerPreferenceActivity.class);
+ intent.putExtra(Apg.EXTRA_KEY_SERVERS, mPreferences.getKeyServers());
+ startActivityForResult(intent, Id.request.key_server_preference);
+ return false;
+ }
+ });
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.request.key_server_preference: {
+ if (resultCode == RESULT_CANCELED || data == null) {
+ return;
+ }
+ String servers[] = data.getStringArrayExtra(Apg.EXTRA_KEY_SERVERS);
+ mPreferences.setKeyServers(servers);
+ mKeyServerPreference.setSummary(getResources().getString(R.string.nKeyServers, servers.length));
+ break;
+ }
+
+ default: {
+ super.onActivityResult(requestCode, resultCode, data);
+ break;
+ }
+ }
+ }
+}
+
diff --git a/org_apg/src/org/apg/ui/PublicKeyListActivity.java b/org_apg/src/org/apg/ui/PublicKeyListActivity.java
new file mode 100644
index 000000000..81a79ce33
--- /dev/null
+++ b/org_apg/src/org/apg/ui/PublicKeyListActivity.java
@@ -0,0 +1,191 @@
+/*
+ * 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.apg.ui;
+
+import org.apg.Apg;
+import org.apg.Constants;
+import org.apg.Id;
+import org.apg.Constants.path;
+import org.apg.Id.menu;
+import org.apg.Id.request;
+import org.apg.Id.type;
+import org.apg.Id.menu.option;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.apg.R;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ExpandableListView;
+import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
+
+public class PublicKeyListActivity extends KeyListActivity {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ mExportFilename = Constants.path.APP_DIR + "/pubexport.asc";
+ mKeyType = Id.type.public_key;
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, Id.menu.option.import_keys, 0, R.string.menu_importKeys).setIcon(
+ android.R.drawable.ic_menu_add);
+ menu.add(0, Id.menu.option.export_keys, 1, R.string.menu_exportKeys).setIcon(
+ android.R.drawable.ic_menu_save);
+ menu.add(1, Id.menu.option.search, 2, R.string.menu_search).setIcon(
+ android.R.drawable.ic_menu_search);
+ menu.add(1, Id.menu.option.preferences, 3, R.string.menu_preferences).setIcon(
+ android.R.drawable.ic_menu_preferences);
+ menu.add(1, Id.menu.option.about, 4, R.string.menu_about).setIcon(
+ android.R.drawable.ic_menu_info_details);
+ menu.add(1, Id.menu.option.scanQRCode, 5, R.string.menu_scanQRCode).setIcon(
+ android.R.drawable.ic_menu_add);
+ return true;
+ }
+
+ @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);
+
+ if (type == ExpandableListView.PACKED_POSITION_TYPE_GROUP) {
+ // TODO: user id? menu.setHeaderTitle("Key");
+ menu.add(0, Id.menu.export, 0, R.string.menu_exportKey);
+ menu.add(0, Id.menu.delete, 1, R.string.menu_deleteKey);
+ menu.add(0, Id.menu.update, 1, R.string.menu_updateKey);
+ menu.add(0, Id.menu.exportToServer, 1, R.string.menu_exportKeyToServer);
+ menu.add(0, Id.menu.signKey, 1, R.string.menu_signKey);
+ }
+ }
+
+ @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 Id.menu.update: {
+ mSelectedItem = groupPosition;
+ final int keyRingId = mListAdapter.getKeyRingId(groupPosition);
+ long keyId = 0;
+ Object keyRing = Apg.getKeyRing(keyRingId);
+ if (keyRing != null && keyRing instanceof PGPPublicKeyRing) {
+ keyId = Apg.getMasterKey((PGPPublicKeyRing) keyRing).getKeyID();
+ }
+ if (keyId == 0) {
+ // this shouldn't happen
+ return true;
+ }
+
+ Intent intent = new Intent(this, KeyServerQueryActivity.class);
+ intent.setAction(Apg.Intent.LOOK_UP_KEY_ID_AND_RETURN);
+ intent.putExtra(Apg.EXTRA_KEY_ID, keyId);
+ startActivityForResult(intent, Id.request.look_up_key_id);
+
+ return true;
+ }
+
+ case Id.menu.exportToServer: {
+ mSelectedItem = groupPosition;
+ final int keyRingId = mListAdapter.getKeyRingId(groupPosition);
+
+ Intent intent = new Intent(this, SendKeyActivity.class);
+ intent.setAction(Apg.Intent.EXPORT_KEY_TO_SERVER);
+ intent.putExtra(Apg.EXTRA_KEY_ID, keyRingId);
+ startActivityForResult(intent, Id.request.export_to_server);
+
+ return true;
+ }
+
+ case Id.menu.signKey: {
+ mSelectedItem = groupPosition;
+ final int keyRingId = mListAdapter.getKeyRingId(groupPosition);
+ long keyId = 0;
+ Object keyRing = Apg.getKeyRing(keyRingId);
+ if (keyRing != null && keyRing instanceof PGPPublicKeyRing) {
+ keyId = Apg.getMasterKey((PGPPublicKeyRing) keyRing).getKeyID();
+ }
+
+ if (keyId == 0) {
+ // this shouldn't happen
+ return true;
+ }
+
+ Intent intent = new Intent(this, SignKeyActivity.class);
+ intent.putExtra(Apg.EXTRA_KEY_ID, keyId);
+ startActivity(intent);
+
+ return true;
+ }
+
+ default: {
+ return super.onContextItemSelected(menuItem);
+ }
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case Id.menu.option.scanQRCode: {
+ Intent intent = new Intent(this, ImportFromQRCodeActivity.class);
+ intent.setAction(Apg.Intent.IMPORT_FROM_QR_CODE);
+ startActivityForResult(intent, Id.request.import_from_qr_code);
+
+ return true;
+ }
+
+ default: {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.request.look_up_key_id: {
+ if (resultCode == RESULT_CANCELED || data == null
+ || data.getStringExtra(Apg.EXTRA_TEXT) == null) {
+ return;
+ }
+
+ Intent intent = new Intent(this, PublicKeyListActivity.class);
+ intent.setAction(Apg.Intent.IMPORT);
+ intent.putExtra(Apg.EXTRA_TEXT, data.getStringExtra(Apg.EXTRA_TEXT));
+ handleIntent(intent);
+ break;
+ }
+
+ default: {
+ super.onActivityResult(requestCode, resultCode, data);
+ break;
+ }
+ }
+ }
+}
diff --git a/org_apg/src/org/apg/ui/SecretKeyListActivity.java b/org_apg/src/org/apg/ui/SecretKeyListActivity.java
new file mode 100644
index 000000000..a5d351bc6
--- /dev/null
+++ b/org_apg/src/org/apg/ui/SecretKeyListActivity.java
@@ -0,0 +1,203 @@
+/*
+ * 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.apg.ui;
+
+import org.apg.Apg;
+import org.apg.AskForSecretKeyPassPhrase;
+import org.apg.Constants;
+import org.apg.Id;
+import org.apg.Constants.path;
+import org.apg.Id.dialog;
+import org.apg.Id.menu;
+import org.apg.Id.message;
+import org.apg.Id.type;
+import org.apg.Id.menu.option;
+import org.apg.R;
+
+import android.app.Dialog;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ExpandableListView;
+import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
+import android.widget.ExpandableListView.OnChildClickListener;
+
+import com.google.zxing.integration.android.IntentIntegrator;
+
+public class SecretKeyListActivity extends KeyListActivity implements OnChildClickListener {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ mExportFilename = Constants.path.APP_DIR + "/secexport.asc";
+ mKeyType = Id.type.secret_key;
+ super.onCreate(savedInstanceState);
+ mList.setOnChildClickListener(this);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, Id.menu.option.import_keys, 0, R.string.menu_importKeys)
+ .setIcon(android.R.drawable.ic_menu_add);
+ menu.add(0, Id.menu.option.export_keys, 1, R.string.menu_exportKeys)
+ .setIcon(android.R.drawable.ic_menu_save);
+ menu.add(1, Id.menu.option.create, 2, R.string.menu_createKey)
+ .setIcon(android.R.drawable.ic_menu_add);
+ menu.add(3, Id.menu.option.search, 3, R.string.menu_search)
+ .setIcon(android.R.drawable.ic_menu_search);
+ menu.add(3, Id.menu.option.preferences, 4, R.string.menu_preferences)
+ .setIcon(android.R.drawable.ic_menu_preferences);
+ menu.add(3, Id.menu.option.about, 5, R.string.menu_about)
+ .setIcon(android.R.drawable.ic_menu_info_details);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case Id.menu.option.create: {
+ createKey();
+ return true;
+ }
+
+ default: {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+ }
+
+ @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);
+
+ if (type == ExpandableListView.PACKED_POSITION_TYPE_GROUP) {
+ // TODO: user id? menu.setHeaderTitle("Key");
+ menu.add(0, Id.menu.edit, 0, R.string.menu_editKey);
+ menu.add(0, Id.menu.export, 1, R.string.menu_exportKey);
+ menu.add(0, Id.menu.delete, 2, R.string.menu_deleteKey);
+ menu.add(0, Id.menu.share, 2, R.string.menu_share);
+ }
+ }
+
+ @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 Id.menu.edit: {
+ mSelectedItem = groupPosition;
+ checkPassPhraseAndEdit();
+ return true;
+ }
+
+ case Id.menu.share: {
+ mSelectedItem = groupPosition;
+
+ long keyId = ((KeyListAdapter) mList.getExpandableListAdapter()).getGroupId(mSelectedItem);
+ String msg = keyId + "," + Apg.getFingerPrint(keyId);;
+
+ new IntentIntegrator(this).shareText(msg);
+ }
+
+ default: {
+ return super.onContextItemSelected(menuItem);
+ }
+ }
+ }
+
+ public boolean onChildClick(ExpandableListView parent, View v, int groupPosition,
+ int childPosition, long id) {
+ mSelectedItem = groupPosition;
+ checkPassPhraseAndEdit();
+ return true;
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case Id.dialog.pass_phrase: {
+ long keyId = ((KeyListAdapter) mList.getExpandableListAdapter()).getGroupId(mSelectedItem);
+ return AskForSecretKeyPassPhrase.createDialog(this, keyId, this);
+ }
+
+ default: {
+ return super.onCreateDialog(id);
+ }
+ }
+ }
+
+ public void checkPassPhraseAndEdit() {
+ long keyId = ((KeyListAdapter) mList.getExpandableListAdapter()).getGroupId(mSelectedItem);
+ String passPhrase = Apg.getCachedPassPhrase(keyId);
+ if (passPhrase == null) {
+ showDialog(Id.dialog.pass_phrase);
+ } else {
+ Apg.setEditPassPhrase(passPhrase);
+ editKey();
+ }
+ }
+
+ @Override
+ public void passPhraseCallback(long keyId, String passPhrase) {
+ super.passPhraseCallback(keyId, passPhrase);
+ Apg.setEditPassPhrase(passPhrase);
+ editKey();
+ }
+
+ private void createKey() {
+ Apg.setEditPassPhrase("");
+ Intent intent = new Intent(this, EditKeyActivity.class);
+ startActivityForResult(intent, Id.message.create_key);
+ }
+
+ private void editKey() {
+ long keyId = ((KeyListAdapter) mList.getExpandableListAdapter()).getGroupId(mSelectedItem);
+ Intent intent = new Intent(this, EditKeyActivity.class);
+ intent.putExtra(Apg.EXTRA_KEY_ID, keyId);
+ startActivityForResult(intent, Id.message.edit_key);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.message.create_key: // intentionally no break
+ case Id.message.edit_key: {
+ if (resultCode == RESULT_OK) {
+ refreshList();
+ }
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+}
diff --git a/org_apg/src/org/apg/ui/SelectPublicKeyListActivity.java b/org_apg/src/org/apg/ui/SelectPublicKeyListActivity.java
new file mode 100644
index 000000000..5216e7a3d
--- /dev/null
+++ b/org_apg/src/org/apg/ui/SelectPublicKeyListActivity.java
@@ -0,0 +1,172 @@
+/*
+ * 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.apg.ui;
+
+import java.util.Vector;
+
+import org.apg.Apg;
+import org.apg.Id;
+import org.apg.Id.menu;
+import org.apg.Id.menu.option;
+import org.apg.R;
+
+import android.app.SearchManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.TextView;
+
+public class SelectPublicKeyListActivity extends BaseActivity {
+ protected ListView mList;
+ protected SelectPublicKeyListAdapter mListAdapter;
+ protected View mFilterLayout;
+ protected Button mClearFilterButton;
+ protected TextView mFilterInfo;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.select_public_key);
+
+ setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
+
+ mList = (ListView) findViewById(R.id.list);
+ // needed in Android 1.5, where the XML attribute gets ignored
+ mList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+
+ Button okButton = (Button) findViewById(R.id.btn_ok);
+ okButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ okClicked();
+ }
+ });
+
+ Button cancelButton = (Button) findViewById(R.id.btn_cancel);
+ cancelButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ cancelClicked();
+ }
+ });
+
+ mFilterLayout = findViewById(R.id.layout_filter);
+ mFilterInfo = (TextView) mFilterLayout.findViewById(R.id.filterInfo);
+ mClearFilterButton = (Button) mFilterLayout.findViewById(R.id.btn_clear);
+
+ mClearFilterButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ handleIntent(new Intent());
+ }
+ });
+
+ handleIntent(getIntent());
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ handleIntent(intent);
+ }
+
+ private void handleIntent(Intent intent) {
+ String searchString = null;
+ if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
+ searchString = intent.getStringExtra(SearchManager.QUERY);
+ if (searchString != null && searchString.trim().length() == 0) {
+ searchString = null;
+ }
+ }
+
+ long selectedKeyIds[] = null;
+ selectedKeyIds = intent.getLongArrayExtra(Apg.EXTRA_SELECTION);
+
+ if (selectedKeyIds == null) {
+ Vector<Long> vector = new Vector<Long>();
+ for (int i = 0; i < mList.getCount(); ++i) {
+ if (mList.isItemChecked(i)) {
+ vector.add(mList.getItemIdAtPosition(i));
+ }
+ }
+ selectedKeyIds = new long[vector.size()];
+ for (int i = 0; i < vector.size(); ++i) {
+ selectedKeyIds[i] = vector.get(i);
+ }
+ }
+
+ if (searchString == null) {
+ mFilterLayout.setVisibility(View.GONE);
+ } else {
+ mFilterLayout.setVisibility(View.VISIBLE);
+ mFilterInfo.setText(getString(R.string.filterInfo, searchString));
+ }
+
+ if (mListAdapter != null) {
+ mListAdapter.cleanup();
+ }
+
+ mListAdapter = new SelectPublicKeyListAdapter(this, mList, searchString, selectedKeyIds);
+ mList.setAdapter(mListAdapter);
+
+ if (selectedKeyIds != null) {
+ for (int i = 0; i < mListAdapter.getCount(); ++i) {
+ long keyId = mListAdapter.getItemId(i);
+ for (int j = 0; j < selectedKeyIds.length; ++j) {
+ if (keyId == selectedKeyIds[j]) {
+ mList.setItemChecked(i, true);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ private void cancelClicked() {
+ setResult(RESULT_CANCELED, null);
+ finish();
+ }
+
+ private void okClicked() {
+ Intent data = new Intent();
+ Vector<Long> keys = new Vector<Long>();
+ Vector<String> userIds = new Vector<String>();
+ for (int i = 0; i < mList.getCount(); ++i) {
+ if (mList.isItemChecked(i)) {
+ keys.add(mList.getItemIdAtPosition(i));
+ userIds.add((String) mList.getItemAtPosition(i));
+ }
+ }
+ long selectedKeyIds[] = new long[keys.size()];
+ for (int i = 0; i < keys.size(); ++i) {
+ selectedKeyIds[i] = keys.get(i);
+ }
+ String userIdArray[] = new String[0];
+ data.putExtra(Apg.EXTRA_SELECTION, selectedKeyIds);
+ data.putExtra(Apg.EXTRA_USER_IDS, userIds.toArray(userIdArray));
+ setResult(RESULT_OK, data);
+ finish();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, Id.menu.option.search, 0, R.string.menu_search)
+ .setIcon(android.R.drawable.ic_menu_search);
+ return true;
+ }
+}
diff --git a/org_apg/src/org/apg/ui/SelectPublicKeyListAdapter.java b/org_apg/src/org/apg/ui/SelectPublicKeyListAdapter.java
new file mode 100644
index 000000000..b2f49f74a
--- /dev/null
+++ b/org_apg/src/org/apg/ui/SelectPublicKeyListAdapter.java
@@ -0,0 +1,226 @@
+/*
+ * 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.apg.ui;
+
+import java.util.Date;
+
+import org.apg.Apg;
+import org.apg.Id;
+import org.apg.Id.database;
+import org.apg.provider.KeyRings;
+import org.apg.provider.Keys;
+import org.apg.provider.UserIds;
+import org.apg.R;
+
+import android.app.Activity;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.CheckBox;
+import android.widget.ListView;
+import android.widget.TextView;
+
+public class SelectPublicKeyListAdapter extends BaseAdapter {
+ protected LayoutInflater mInflater;
+ protected ListView mParent;
+ protected SQLiteDatabase mDatabase;
+ protected Cursor mCursor;
+ protected String mSearchString;
+ protected Activity mActivity;
+
+ public SelectPublicKeyListAdapter(Activity activity, ListView parent,
+ String searchString, long selectedKeyIds[]) {
+ mSearchString = searchString;
+
+ mActivity = activity;
+ mParent = parent;
+ mDatabase = Apg.getDatabase().db();
+ mInflater = (LayoutInflater) parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ long now = new Date().getTime() / 1000;
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables(KeyRings.TABLE_NAME + " INNER JOIN " + Keys.TABLE_NAME + " ON " +
+ "(" + KeyRings.TABLE_NAME + "." + KeyRings._ID + " = " +
+ Keys.TABLE_NAME + "." + Keys.KEY_RING_ID + " AND " +
+ Keys.TABLE_NAME + "." + Keys.IS_MASTER_KEY + " = '1'" +
+ ") " +
+ " INNER JOIN " + UserIds.TABLE_NAME + " ON " +
+ "(" + Keys.TABLE_NAME + "." + Keys._ID + " = " +
+ UserIds.TABLE_NAME + "." + UserIds.KEY_ID + " AND " +
+ UserIds.TABLE_NAME + "." + UserIds.RANK + " = '0') ");
+
+ String inIdList = null;
+
+ if (selectedKeyIds != null && selectedKeyIds.length > 0) {
+ inIdList = KeyRings.TABLE_NAME + "." + KeyRings.MASTER_KEY_ID + " IN (";
+ for (int i = 0; i < selectedKeyIds.length; ++i) {
+ if (i != 0) {
+ inIdList += ", ";
+ }
+ inIdList += DatabaseUtils.sqlEscapeString("" + selectedKeyIds[i]);
+ }
+ inIdList += ")";
+ }
+
+ if (searchString != null && searchString.trim().length() > 0) {
+ String[] chunks = searchString.trim().split(" +");
+ qb.appendWhere("(EXISTS (SELECT tmp." + UserIds._ID + " FROM " +
+ UserIds.TABLE_NAME + " AS tmp WHERE " +
+ "tmp." + UserIds.KEY_ID + " = " +
+ Keys.TABLE_NAME + "." + Keys._ID);
+ for (int i = 0; i < chunks.length; ++i) {
+ qb.appendWhere(" AND tmp." + UserIds.USER_ID + " LIKE ");
+ qb.appendWhereEscapeString("%" + chunks[i] + "%");
+ }
+ qb.appendWhere("))");
+
+ if (inIdList != null) {
+ qb.appendWhere(" OR (" + inIdList + ")");
+ }
+ }
+
+ String orderBy = UserIds.TABLE_NAME + "." + UserIds.USER_ID + " ASC";
+ if (inIdList != null) {
+ orderBy = inIdList + " DESC, " + orderBy;
+ }
+
+ mCursor = qb.query(mDatabase,
+ new String[] {
+ KeyRings.TABLE_NAME + "." + KeyRings._ID, // 0
+ KeyRings.TABLE_NAME + "." + KeyRings.MASTER_KEY_ID, // 1
+ UserIds.TABLE_NAME + "." + UserIds.USER_ID, // 2
+ "(SELECT COUNT(tmp." + Keys._ID + ") FROM " + Keys.TABLE_NAME + " AS tmp WHERE " +
+ "tmp." + Keys.KEY_RING_ID + " = " +
+ KeyRings.TABLE_NAME + "." + KeyRings._ID + " AND " +
+ "tmp." + Keys.IS_REVOKED + " = '0' AND " +
+ "tmp." + Keys.CAN_ENCRYPT + " = '1')", // 3
+ "(SELECT COUNT(tmp." + Keys._ID + ") FROM " + Keys.TABLE_NAME + " AS tmp WHERE " +
+ "tmp." + Keys.KEY_RING_ID + " = " +
+ KeyRings.TABLE_NAME + "." + KeyRings._ID + " AND " +
+ "tmp." + Keys.IS_REVOKED + " = '0' AND " +
+ "tmp." + Keys.CAN_ENCRYPT + " = '1' AND " +
+ "tmp." + Keys.CREATION + " <= '" + now + "' AND " +
+ "(tmp." + Keys.EXPIRY + " IS NULL OR " +
+ "tmp." + Keys.EXPIRY + " >= '" + now + "'))", // 4
+ },
+ KeyRings.TABLE_NAME + "." + KeyRings.TYPE + " = ?",
+ new String[] { "" + Id.database.type_public },
+ null, null, orderBy);
+
+ activity.startManagingCursor(mCursor);
+ }
+
+ public void cleanup() {
+ if (mCursor != null) {
+ mActivity.stopManagingCursor(mCursor);
+ mCursor.close();
+ }
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ mCursor.moveToPosition(position);
+ return mCursor.getInt(4) > 0; // valid CAN_ENCRYPT
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ public int getCount() {
+ return mCursor.getCount();
+ }
+
+ public Object getItem(int position) {
+ mCursor.moveToPosition(position);
+ return mCursor.getString(2); // USER_ID
+ }
+
+ public long getItemId(int position) {
+ mCursor.moveToPosition(position);
+ return mCursor.getLong(1); // MASTER_KEY_ID
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ mCursor.moveToPosition(position);
+
+ View view = mInflater.inflate(R.layout.select_public_key_item, null);
+ boolean enabled = isEnabled(position);
+
+ TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId);
+ mainUserId.setText(R.string.unknownUserId);
+ TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest);
+ mainUserIdRest.setText("");
+ TextView keyId = (TextView) view.findViewById(R.id.keyId);
+ keyId.setText(R.string.noKey);
+ TextView status = (TextView) view.findViewById(R.id.status);
+ status.setText(R.string.unknownStatus);
+
+ String userId = mCursor.getString(2); // USER_ID
+ if (userId != null) {
+ String chunks[] = userId.split(" <", 2);
+ userId = chunks[0];
+ if (chunks.length > 1) {
+ mainUserIdRest.setText("<" + chunks[1]);
+ }
+ mainUserId.setText(userId);
+ }
+
+ long masterKeyId = mCursor.getLong(1); // MASTER_KEY_ID
+ keyId.setText(Apg.getSmallFingerPrint(masterKeyId));
+
+ if (mainUserIdRest.getText().length() == 0) {
+ mainUserIdRest.setVisibility(View.GONE);
+ }
+
+ if (enabled) {
+ status.setText(R.string.canEncrypt);
+ } else {
+ if (mCursor.getInt(3) > 0) {
+ // has some CAN_ENCRYPT keys, but col(4) = 0, so must be revoked or expired
+ status.setText(R.string.expired);
+ } else {
+ status.setText(R.string.noKey);
+ }
+ }
+
+ status.setText(status.getText() + " ");
+
+ CheckBox selected = (CheckBox) view.findViewById(R.id.selected);
+
+ if (!enabled) {
+ mParent.setItemChecked(position, false);
+ }
+
+ selected.setChecked(mParent.isItemChecked(position));
+
+ view.setEnabled(enabled);
+ mainUserId.setEnabled(enabled);
+ mainUserIdRest.setEnabled(enabled);
+ keyId.setEnabled(enabled);
+ selected.setEnabled(enabled);
+ status.setEnabled(enabled);
+
+ return view;
+ }
+}
diff --git a/org_apg/src/org/apg/ui/SelectSecretKeyListActivity.java b/org_apg/src/org/apg/ui/SelectSecretKeyListActivity.java
new file mode 100644
index 000000000..191a0ecc7
--- /dev/null
+++ b/org_apg/src/org/apg/ui/SelectSecretKeyListActivity.java
@@ -0,0 +1,115 @@
+/*
+ * 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.apg.ui;
+
+import org.apg.Apg;
+import org.apg.Id;
+import org.apg.Id.menu;
+import org.apg.Id.menu.option;
+import org.apg.R;
+
+import android.app.SearchManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.TextView;
+
+public class SelectSecretKeyListActivity extends BaseActivity {
+ protected ListView mList;
+ protected SelectSecretKeyListAdapter mListAdapter;
+ protected View mFilterLayout;
+ protected Button mClearFilterButton;
+ protected TextView mFilterInfo;
+
+ protected long mSelectedKeyId = 0;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
+
+ setContentView(R.layout.select_secret_key);
+
+ mList = (ListView) findViewById(R.id.list);
+
+ mList.setOnItemClickListener(new OnItemClickListener() {
+ public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
+ Intent data = new Intent();
+ data.putExtra(Apg.EXTRA_KEY_ID, id);
+ data.putExtra(Apg.EXTRA_USER_ID, (String)mList.getItemAtPosition(position));
+ setResult(RESULT_OK, data);
+ finish();
+ }
+ });
+
+ mFilterLayout = findViewById(R.id.layout_filter);
+ mFilterInfo = (TextView) mFilterLayout.findViewById(R.id.filterInfo);
+ mClearFilterButton = (Button) mFilterLayout.findViewById(R.id.btn_clear);
+
+ mClearFilterButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ handleIntent(new Intent());
+ }
+ });
+
+ handleIntent(getIntent());
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ handleIntent(intent);
+ }
+
+ private void handleIntent(Intent intent) {
+ String searchString = null;
+ if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
+ searchString = intent.getStringExtra(SearchManager.QUERY);
+ if (searchString != null && searchString.trim().length() == 0) {
+ searchString = null;
+ }
+ }
+
+ if (searchString == null) {
+ mFilterLayout.setVisibility(View.GONE);
+ } else {
+ mFilterLayout.setVisibility(View.VISIBLE);
+ mFilterInfo.setText(getString(R.string.filterInfo, searchString));
+ }
+
+ if (mListAdapter != null) {
+ mListAdapter.cleanup();
+ }
+
+ mListAdapter = new SelectSecretKeyListAdapter(this, mList, searchString);
+ mList.setAdapter(mListAdapter);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, Id.menu.option.search, 0, R.string.menu_search)
+ .setIcon(android.R.drawable.ic_menu_search);
+ return true;
+ }
+}
diff --git a/org_apg/src/org/apg/ui/SelectSecretKeyListAdapter.java b/org_apg/src/org/apg/ui/SelectSecretKeyListAdapter.java
new file mode 100644
index 000000000..1a7734245
--- /dev/null
+++ b/org_apg/src/org/apg/ui/SelectSecretKeyListAdapter.java
@@ -0,0 +1,176 @@
+package org.apg.ui;
+
+import java.util.Date;
+
+import org.apg.Apg;
+import org.apg.Id;
+import org.apg.Id.database;
+import org.apg.provider.KeyRings;
+import org.apg.provider.Keys;
+import org.apg.provider.UserIds;
+import org.apg.R;
+
+import android.app.Activity;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+public class SelectSecretKeyListAdapter extends BaseAdapter {
+ protected LayoutInflater mInflater;
+ protected ListView mParent;
+ protected SQLiteDatabase mDatabase;
+ protected Cursor mCursor;
+ protected String mSearchString;
+ protected Activity mActivity;
+
+ public SelectSecretKeyListAdapter(Activity activity, ListView parent, String searchString) {
+ mSearchString = searchString;
+
+ mActivity = activity;
+ mParent = parent;
+ mDatabase = Apg.getDatabase().db();
+ mInflater = (LayoutInflater) parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ long now = new Date().getTime() / 1000;
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables(KeyRings.TABLE_NAME + " INNER JOIN " + Keys.TABLE_NAME + " ON " +
+ "(" + KeyRings.TABLE_NAME + "." + KeyRings._ID + " = " +
+ Keys.TABLE_NAME + "." + Keys.KEY_RING_ID + " AND " +
+ Keys.TABLE_NAME + "." + Keys.IS_MASTER_KEY + " = '1'" +
+ ") " +
+ " INNER JOIN " + UserIds.TABLE_NAME + " ON " +
+ "(" + Keys.TABLE_NAME + "." + Keys._ID + " = " +
+ UserIds.TABLE_NAME + "." + UserIds.KEY_ID + " AND " +
+ UserIds.TABLE_NAME + "." + UserIds.RANK + " = '0') ");
+
+ if (searchString != null && searchString.trim().length() > 0) {
+ String[] chunks = searchString.trim().split(" +");
+ qb.appendWhere("EXISTS (SELECT tmp." + UserIds._ID + " FROM " +
+ UserIds.TABLE_NAME + " AS tmp WHERE " +
+ "tmp." + UserIds.KEY_ID + " = " +
+ Keys.TABLE_NAME + "." + Keys._ID);
+ for (int i = 0; i < chunks.length; ++i) {
+ qb.appendWhere(" AND tmp." + UserIds.USER_ID + " LIKE ");
+ qb.appendWhereEscapeString("%" + chunks[i] + "%");
+ }
+ qb.appendWhere(")");
+ }
+
+ mCursor = qb.query(mDatabase,
+ new String[] {
+ KeyRings.TABLE_NAME + "." + KeyRings._ID, // 0
+ KeyRings.TABLE_NAME + "." + KeyRings.MASTER_KEY_ID, // 1
+ UserIds.TABLE_NAME + "." + UserIds.USER_ID, // 2
+ "(SELECT COUNT(tmp." + Keys._ID + ") FROM " + Keys.TABLE_NAME + " AS tmp WHERE " +
+ "tmp." + Keys.KEY_RING_ID + " = " +
+ KeyRings.TABLE_NAME + "." + KeyRings._ID + " AND " +
+ "tmp." + Keys.IS_REVOKED + " = '0' AND " +
+ "tmp." + Keys.CAN_SIGN + " = '1')", // 3,
+ "(SELECT COUNT(tmp." + Keys._ID + ") FROM " + Keys.TABLE_NAME + " AS tmp WHERE " +
+ "tmp." + Keys.KEY_RING_ID + " = " +
+ KeyRings.TABLE_NAME + "." + KeyRings._ID + " AND " +
+ "tmp." + Keys.IS_REVOKED + " = '0' AND " +
+ "tmp." + Keys.CAN_SIGN + " = '1' AND " +
+ "tmp." + Keys.CREATION + " <= '" + now + "' AND " +
+ "(tmp." + Keys.EXPIRY + " IS NULL OR " +
+ "tmp." + Keys.EXPIRY + " >= '" + now + "'))", // 4
+ },
+ KeyRings.TABLE_NAME + "." + KeyRings.TYPE + " = ?",
+ new String[] { "" + Id.database.type_secret },
+ null, null, UserIds.TABLE_NAME + "." + UserIds.USER_ID + " ASC");
+
+ activity.startManagingCursor(mCursor);
+ }
+
+ public void cleanup() {
+ if (mCursor != null) {
+ mActivity.stopManagingCursor(mCursor);
+ mCursor.close();
+ }
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ mCursor.moveToPosition(position);
+ return mCursor.getInt(4) > 0; // valid CAN_SIGN
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ public int getCount() {
+ return mCursor.getCount();
+ }
+
+ public Object getItem(int position) {
+ mCursor.moveToPosition(position);
+ return mCursor.getString(2); // USER_ID
+ }
+
+ public long getItemId(int position) {
+ mCursor.moveToPosition(position);
+ return mCursor.getLong(1); // MASTER_KEY_ID
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ mCursor.moveToPosition(position);
+
+ View view = mInflater.inflate(R.layout.select_secret_key_item, null);
+ boolean enabled = isEnabled(position);
+
+ TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId);
+ mainUserId.setText(R.string.unknownUserId);
+ TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest);
+ mainUserIdRest.setText("");
+ TextView keyId = (TextView) view.findViewById(R.id.keyId);
+ keyId.setText(R.string.noKey);
+ TextView status = (TextView) view.findViewById(R.id.status);
+ status.setText(R.string.unknownStatus);
+
+ String userId = mCursor.getString(2); // USER_ID
+ if (userId != null) {
+ String chunks[] = userId.split(" <", 2);
+ userId = chunks[0];
+ if (chunks.length > 1) {
+ mainUserIdRest.setText("<" + chunks[1]);
+ }
+ mainUserId.setText(userId);
+ }
+
+ long masterKeyId = mCursor.getLong(1); // MASTER_KEY_ID
+ keyId.setText(Apg.getSmallFingerPrint(masterKeyId));
+
+ if (mainUserIdRest.getText().length() == 0) {
+ mainUserIdRest.setVisibility(View.GONE);
+ }
+
+ if (enabled) {
+ status.setText(R.string.canSign);
+ } else {
+ if (mCursor.getInt(3) > 0) {
+ // has some CAN_SIGN keys, but col(4) = 0, so must be revoked or expired
+ status.setText(R.string.expired);
+ } else {
+ status.setText(R.string.noKey);
+ }
+ }
+
+ status.setText(status.getText() + " ");
+
+ view.setEnabled(enabled);
+ mainUserId.setEnabled(enabled);
+ mainUserIdRest.setEnabled(enabled);
+ keyId.setEnabled(enabled);
+ status.setEnabled(enabled);
+
+ return view;
+ }
+} \ No newline at end of file
diff --git a/org_apg/src/org/apg/ui/SendKeyActivity.java b/org_apg/src/org/apg/ui/SendKeyActivity.java
new file mode 100644
index 000000000..c44e87469
--- /dev/null
+++ b/org_apg/src/org/apg/ui/SendKeyActivity.java
@@ -0,0 +1,100 @@
+package org.apg.ui;
+
+import org.apg.Apg;
+import org.apg.Constants;
+import org.apg.HkpKeyServer;
+import org.apg.Id;
+import org.apg.Constants.extras;
+import org.apg.Id.message;
+import org.spongycastle.openpgp.PGPKeyRing;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.apg.R;
+
+import android.os.Bundle;
+import android.os.Message;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.Spinner;
+import android.widget.Toast;
+
+/**
+ * gpg --send-key activity
+ *
+ * Sends the selected public key to a key server
+ */
+public class SendKeyActivity extends BaseActivity {
+
+ private Button export;
+ private Spinner keyServer;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.key_server_export_layout);
+
+ export = (Button) findViewById(R.id.btn_export_to_server);
+ keyServer = (Spinner) findViewById(R.id.keyServer);
+
+ ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, mPreferences.getKeyServers());
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ keyServer.setAdapter(adapter);
+ if (adapter.getCount() > 0) {
+ keyServer.setSelection(0);
+ } else {
+ export.setEnabled(false);
+ }
+
+ export.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startThread();
+ }
+ });
+ }
+
+ @Override
+ public void run() {
+ String error = null;
+ Bundle data = new Bundle();
+ Message msg = new Message();
+
+ HkpKeyServer server = new HkpKeyServer((String) keyServer.getSelectedItem());
+
+ int keyRingId = getIntent().getIntExtra(Apg.EXTRA_KEY_ID, -1);
+
+ PGPKeyRing keyring = Apg.getKeyRing(keyRingId);
+ if (keyring != null && keyring instanceof PGPPublicKeyRing) {
+ boolean uploaded = Apg.uploadKeyRingToServer(server, (PGPPublicKeyRing) keyring);
+ if (!uploaded) {
+ error = "Unable to export key to selected server";
+ }
+ }
+
+ data.putInt(Constants.extras.STATUS, Id.message.export_done);
+
+ if (error != null) {
+ data.putString(Apg.EXTRA_ERROR, error);
+ }
+
+ msg.setData(data);
+ sendMessage(msg);
+ }
+
+ @Override
+ public void doneCallback(Message msg) {
+ super.doneCallback(msg);
+
+ Bundle data = msg.getData();
+ String error = data.getString(Apg.EXTRA_ERROR);
+ if (error != null) {
+ Toast.makeText(this, getString(R.string.errorMessage, error), Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Toast.makeText(this, R.string.keySendSuccess, Toast.LENGTH_SHORT).show();
+ finish();
+ }
+}
diff --git a/org_apg/src/org/apg/ui/SignKeyActivity.java b/org_apg/src/org/apg/ui/SignKeyActivity.java
new file mode 100644
index 000000000..ab145c921
--- /dev/null
+++ b/org_apg/src/org/apg/ui/SignKeyActivity.java
@@ -0,0 +1,294 @@
+package org.apg.ui;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SignatureException;
+import java.util.Iterator;
+
+import org.apg.Apg;
+import org.apg.Constants;
+import org.apg.HkpKeyServer;
+import org.apg.Id;
+import org.apg.Constants.extras;
+import org.apg.Id.dialog;
+import org.apg.Id.message;
+import org.apg.Id.request;
+import org.apg.Id.return_value;
+import org.spongycastle.jce.provider.BouncyCastleProvider;
+import org.spongycastle.openpgp.PGPException;
+import org.spongycastle.openpgp.PGPPrivateKey;
+import org.spongycastle.openpgp.PGPPublicKey;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSignature;
+import org.spongycastle.openpgp.PGPSignatureGenerator;
+import org.spongycastle.openpgp.PGPSignatureSubpacketGenerator;
+import org.spongycastle.openpgp.PGPSignatureSubpacketVector;
+import org.spongycastle.openpgp.PGPUtil;
+import org.apg.R;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Message;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.Spinner;
+import android.widget.Toast;
+
+/**
+ * gpg --sign-key
+ *
+ * signs the specified public key with the specified secret master key
+ */
+public class SignKeyActivity extends BaseActivity {
+ private static final String TAG = "SignKeyActivity";
+
+ private long pubKeyId = 0;
+ private long masterKeyId = 0;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // check we havent already signed it
+ setContentView(R.layout.sign_key_layout);
+
+ final Spinner keyServer = (Spinner) findViewById(R.id.keyServer);
+ ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, mPreferences.getKeyServers());
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ keyServer.setAdapter(adapter);
+
+ final CheckBox sendKey = (CheckBox) findViewById(R.id.sendKey);
+ if (!sendKey.isChecked()) {
+ keyServer.setEnabled(false);
+ } else {
+ keyServer.setEnabled(true);
+ }
+
+ sendKey.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ if (!isChecked) {
+ keyServer.setEnabled(false);
+ } else {
+ keyServer.setEnabled(true);
+ }
+ }
+ });
+
+ Button sign = (Button) findViewById(R.id.sign);
+ sign.setEnabled(false); // disabled until the user selects a key to sign with
+ sign.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (pubKeyId != 0) {
+ initiateSigning();
+ }
+ }
+ });
+
+ pubKeyId = getIntent().getLongExtra(Apg.EXTRA_KEY_ID, 0);
+ if (pubKeyId == 0) {
+ finish(); // nothing to do if we dont know what key to sign
+ } else {
+ // kick off the SecretKey selection activity so the user chooses which key to sign with first
+ Intent intent = new Intent(this, SelectSecretKeyListActivity.class);
+ startActivityForResult(intent, Id.request.secret_keys);
+ }
+ }
+
+ /**
+ * handles the UI bits of the signing process on the UI thread
+ */
+ private void initiateSigning() {
+ PGPPublicKeyRing pubring = Apg.getPublicKeyRing(pubKeyId);
+ if (pubring != null) {
+ // if we have already signed this key, dont bother doing it again
+ boolean alreadySigned = false;
+
+ @SuppressWarnings("unchecked")
+ Iterator<PGPSignature> itr = pubring.getPublicKey(pubKeyId).getSignatures();
+ while (itr.hasNext()) {
+ PGPSignature sig = itr.next();
+ if (sig.getKeyID() == masterKeyId) {
+ alreadySigned = true;
+ break;
+ }
+ }
+
+ if (!alreadySigned) {
+ /*
+ * get the user's passphrase for this key (if required)
+ */
+ String passphrase = Apg.getCachedPassPhrase(masterKeyId);
+ if (passphrase == null) {
+ showDialog(Id.dialog.pass_phrase);
+ return; // bail out; need to wait until the user has entered the passphrase before trying again
+ } else {
+ startSigning();
+ }
+ } else {
+ final Bundle status = new Bundle();
+ Message msg = new Message();
+
+ status.putString(Apg.EXTRA_ERROR, "Key has already been signed");
+
+ status.putInt(Constants.extras.STATUS, Id.message.done);
+
+ msg.setData(status);
+ sendMessage(msg);
+
+ setResult(Id.return_value.error);
+ finish();
+ }
+ }
+ }
+
+ @Override
+ public long getSecretKeyId() {
+ return masterKeyId;
+ }
+
+ @Override
+ public void passPhraseCallback(long keyId, String passPhrase) {
+ super.passPhraseCallback(keyId, passPhrase);
+ startSigning();
+ }
+
+ /**
+ * kicks off the actual signing process on a background thread
+ */
+ private void startSigning() {
+ showDialog(Id.dialog.signing);
+ startThread();
+ }
+
+ @Override
+ public void run() {
+ final Bundle status = new Bundle();
+ Message msg = new Message();
+
+ try {
+ String passphrase = Apg.getCachedPassPhrase(masterKeyId);
+ if (passphrase == null || passphrase.length() <= 0) {
+ status.putString(Apg.EXTRA_ERROR, "Unable to obtain passphrase");
+ } else {
+ PGPPublicKeyRing pubring = Apg.getPublicKeyRing(pubKeyId);
+
+ /*
+ * sign the incoming key
+ */
+ PGPSecretKey secretKey = Apg.getSecretKey(masterKeyId);
+ PGPPrivateKey signingKey = secretKey.extractPrivateKey(passphrase.toCharArray(), BouncyCastleProvider.PROVIDER_NAME);
+ PGPSignatureGenerator sGen = new PGPSignatureGenerator(secretKey.getPublicKey().getAlgorithm(), PGPUtil.SHA256, BouncyCastleProvider.PROVIDER_NAME);
+ sGen.initSign(PGPSignature.DIRECT_KEY, signingKey);
+
+ PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
+
+ PGPSignatureSubpacketVector packetVector = spGen.generate();
+ sGen.setHashedSubpackets(packetVector);
+
+ PGPPublicKey signedKey = PGPPublicKey.addCertification(pubring.getPublicKey(pubKeyId), sGen.generate());
+ pubring = PGPPublicKeyRing.insertPublicKey(pubring, signedKey);
+
+ // check if we need to send the key to the server or not
+ CheckBox sendKey = (CheckBox) findViewById(R.id.sendKey);
+ if (sendKey.isChecked()) {
+ Spinner keyServer = (Spinner) findViewById(R.id.keyServer);
+ HkpKeyServer server = new HkpKeyServer((String) keyServer.getSelectedItem());
+
+ /*
+ * upload the newly signed key to the key server
+ */
+
+ Apg.uploadKeyRingToServer(server, pubring);
+ }
+
+ // store the signed key in our local cache
+ int retval = Apg.storeKeyRingInCache(pubring);
+ if (retval != Id.return_value.ok && retval != Id.return_value.updated) {
+ status.putString(Apg.EXTRA_ERROR, "Failed to store signed key in local cache");
+ }
+ }
+ } catch (PGPException e) {
+ Log.e(TAG, "Failed to sign key", e);
+ status.putString(Apg.EXTRA_ERROR, "Failed to sign key");
+ status.putInt(Constants.extras.STATUS, Id.message.done);
+ return;
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(TAG, "Failed to sign key", e);
+ status.putString(Apg.EXTRA_ERROR, "Failed to sign key");
+ status.putInt(Constants.extras.STATUS, Id.message.done);
+ return;
+ } catch (NoSuchProviderException e) {
+ Log.e(TAG, "Failed to sign key", e);
+ status.putString(Apg.EXTRA_ERROR, "Failed to sign key");
+ status.putInt(Constants.extras.STATUS, Id.message.done);
+ return;
+ } catch (SignatureException e) {
+ Log.e(TAG, "Failed to sign key", e);
+ status.putString(Apg.EXTRA_ERROR, "Failed to sign key");
+ status.putInt(Constants.extras.STATUS, Id.message.done);
+ return;
+ }
+
+ status.putInt(Constants.extras.STATUS, Id.message.done);
+
+ msg.setData(status);
+ sendMessage(msg);
+
+ if (status.containsKey(Apg.EXTRA_ERROR)) {
+ setResult(Id.return_value.error);
+ } else {
+ setResult(Id.return_value.ok);
+ }
+
+ finish();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.request.secret_keys: {
+ if (resultCode == RESULT_OK) {
+ masterKeyId = data.getLongExtra(Apg.EXTRA_KEY_ID, 0);
+
+ // re-enable the sign button so the user can initiate the sign process
+ Button sign = (Button) findViewById(R.id.sign);
+ sign.setEnabled(true);
+ }
+
+ break;
+ }
+
+ default: {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+ }
+
+ @Override
+ public void doneCallback(Message msg) {
+ super.doneCallback(msg);
+
+ removeDialog(Id.dialog.signing);
+
+ Bundle data = msg.getData();
+ String error = data.getString(Apg.EXTRA_ERROR);
+ if (error != null) {
+ Toast.makeText(this, getString(R.string.errorMessage, error), Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Toast.makeText(this, R.string.keySignSuccess, Toast.LENGTH_SHORT).show();
+ finish();
+ }
+}
diff --git a/org_apg/src/org/apg/ui/widget/Editor.java b/org_apg/src/org/apg/ui/widget/Editor.java
new file mode 100644
index 000000000..be95ad656
--- /dev/null
+++ b/org_apg/src/org/apg/ui/widget/Editor.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.ui.widget;
+
+public interface Editor {
+ public interface EditorListener {
+ public void onDeleted(Editor editor);
+ }
+
+ public void setEditorListener(EditorListener listener);
+}
diff --git a/org_apg/src/org/apg/ui/widget/IntegerListPreference.java b/org_apg/src/org/apg/ui/widget/IntegerListPreference.java
new file mode 100644
index 000000000..fa411a786
--- /dev/null
+++ b/org_apg/src/org/apg/ui/widget/IntegerListPreference.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apg.ui.widget;
+
+import android.content.Context;
+import android.preference.ListPreference;
+import android.util.AttributeSet;
+
+/**
+ * A list preference which persists its values as integers instead of strings.
+ * Code reading the values should use
+ * {@link android.content.SharedPreferences#getInt}.
+ * When using XML-declared arrays for entry values, the arrays should be regular
+ * string arrays containing valid integer values.
+ *
+ * @author Rodrigo Damazio
+ */
+public class IntegerListPreference extends ListPreference {
+
+ public IntegerListPreference(Context context) {
+ super(context);
+
+ verifyEntryValues(null);
+ }
+
+ public IntegerListPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ verifyEntryValues(null);
+ }
+
+ @Override
+ public void setEntryValues(CharSequence[] entryValues) {
+ CharSequence[] oldValues = getEntryValues();
+ super.setEntryValues(entryValues);
+ verifyEntryValues(oldValues);
+ }
+
+ @Override
+ public void setEntryValues(int entryValuesResId) {
+ CharSequence[] oldValues = getEntryValues();
+ super.setEntryValues(entryValuesResId);
+ verifyEntryValues(oldValues);
+ }
+
+ @Override
+ protected String getPersistedString(String defaultReturnValue) {
+ // During initial load, there's no known default value
+ int defaultIntegerValue = Integer.MIN_VALUE;
+ if (defaultReturnValue != null) {
+ defaultIntegerValue = Integer.parseInt(defaultReturnValue);
+ }
+
+ // When the list preference asks us to read a string, instead read an
+ // integer.
+ int value = getPersistedInt(defaultIntegerValue);
+ return Integer.toString(value);
+ }
+
+ @Override
+ protected boolean persistString(String value) {
+ // When asked to save a string, instead save an integer
+ return persistInt(Integer.parseInt(value));
+ }
+
+ private void verifyEntryValues(CharSequence[] oldValues) {
+ CharSequence[] entryValues = getEntryValues();
+ if (entryValues == null) {
+ return;
+ }
+
+ for (CharSequence entryValue : entryValues) {
+ try {
+ Integer.parseInt(entryValue.toString());
+ } catch (NumberFormatException nfe) {
+ super.setEntryValues(oldValues);
+ throw nfe;
+ }
+ }
+ }
+}
diff --git a/org_apg/src/org/apg/ui/widget/KeyEditor.java b/org_apg/src/org/apg/ui/widget/KeyEditor.java
new file mode 100644
index 000000000..ef98f794a
--- /dev/null
+++ b/org_apg/src/org/apg/ui/widget/KeyEditor.java
@@ -0,0 +1,233 @@
+/*
+ * 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.apg.ui.widget;
+
+import org.apg.Apg;
+import org.apg.Id;
+import org.apg.util.Choice;
+import org.spongycastle.openpgp.PGPPublicKey;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.apg.R;
+
+import android.app.DatePickerDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.DatePicker;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import java.text.DateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Vector;
+
+public class KeyEditor extends LinearLayout implements Editor, OnClickListener {
+ private PGPSecretKey mKey;
+
+ private EditorListener mEditorListener = null;
+
+ private boolean mIsMasterKey;
+ ImageButton mDeleteButton;
+ TextView mAlgorithm;
+ TextView mKeyId;
+ Spinner mUsage;
+ TextView mCreationDate;
+ Button mExpiryDateButton;
+ GregorianCalendar mExpiryDate;
+
+ private DatePickerDialog.OnDateSetListener mExpiryDateSetListener =
+ new DatePickerDialog.OnDateSetListener() {
+ public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
+ GregorianCalendar date = new GregorianCalendar(year, monthOfYear, dayOfMonth);
+ setExpiryDate(date);
+ }
+ };
+
+ public KeyEditor(Context context) {
+ super(context);
+ }
+
+ public KeyEditor(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ setDrawingCacheEnabled(true);
+ setAlwaysDrawnWithCacheEnabled(true);
+
+ mAlgorithm = (TextView) findViewById(R.id.algorithm);
+ mKeyId = (TextView) findViewById(R.id.keyId);
+ mCreationDate = (TextView) findViewById(R.id.creation);
+ mExpiryDateButton = (Button) findViewById(R.id.expiry);
+ mUsage = (Spinner) findViewById(R.id.usage);
+ Choice choices[] = {
+ new Choice(Id.choice.usage.sign_only,
+ getResources().getString(R.string.choice_signOnly)),
+ new Choice(Id.choice.usage.encrypt_only,
+ getResources().getString(R.string.choice_encryptOnly)),
+ new Choice(Id.choice.usage.sign_and_encrypt,
+ getResources().getString(R.string.choice_signAndEncrypt)),
+ };
+ ArrayAdapter<Choice> adapter =
+ new ArrayAdapter<Choice>(getContext(),
+ android.R.layout.simple_spinner_item, choices);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mUsage.setAdapter(adapter);
+
+ mDeleteButton = (ImageButton) findViewById(R.id.delete);
+ mDeleteButton.setOnClickListener(this);
+
+ setExpiryDate(null);
+
+ mExpiryDateButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ GregorianCalendar date = mExpiryDate;
+ if (date == null) {
+ date = new GregorianCalendar();
+ }
+
+ DatePickerDialog dialog =
+ new DatePickerDialog(getContext(), mExpiryDateSetListener,
+ date.get(Calendar.YEAR),
+ date.get(Calendar.MONTH),
+ date.get(Calendar.DAY_OF_MONTH));
+ dialog.setCancelable(true);
+ dialog.setButton(Dialog.BUTTON_NEGATIVE,
+ getContext().getString(R.string.btn_noDate),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ setExpiryDate(null);
+ }
+ });
+ dialog.show();
+ }
+ });
+
+ super.onFinishInflate();
+ }
+
+ public void setValue(PGPSecretKey key, boolean isMasterKey) {
+ mKey = key;
+
+ mIsMasterKey = isMasterKey;
+ if (mIsMasterKey) {
+ mDeleteButton.setVisibility(View.INVISIBLE);
+ }
+
+ mAlgorithm.setText(Apg.getAlgorithmInfo(key));
+ String keyId1Str = Apg.getSmallFingerPrint(key.getKeyID());
+ String keyId2Str = Apg.getSmallFingerPrint(key.getKeyID() >> 32);
+ mKeyId.setText(keyId1Str + " " + keyId2Str);
+
+ Vector<Choice> choices = new Vector<Choice>();
+ boolean isElGamalKey = (key.getPublicKey().getAlgorithm() == PGPPublicKey.ELGAMAL_ENCRYPT);
+ if (!isElGamalKey) {
+ choices.add(new Choice(Id.choice.usage.sign_only,
+ getResources().getString(R.string.choice_signOnly)));
+ }
+ if (!mIsMasterKey) {
+ choices.add(new Choice(Id.choice.usage.encrypt_only,
+ getResources().getString(R.string.choice_encryptOnly)));
+ }
+ if (!isElGamalKey) {
+ choices.add(new Choice(Id.choice.usage.sign_and_encrypt,
+ getResources().getString(R.string.choice_signAndEncrypt)));
+ }
+
+ ArrayAdapter<Choice> adapter =
+ new ArrayAdapter<Choice>(getContext(),
+ android.R.layout.simple_spinner_item, choices);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mUsage.setAdapter(adapter);
+
+ int selectId = 0;
+ if (Apg.isEncryptionKey(key)) {
+ if (Apg.isSigningKey(key)) {
+ selectId = Id.choice.usage.sign_and_encrypt;
+ } else {
+ selectId = Id.choice.usage.encrypt_only;
+ }
+ } else {
+ selectId = Id.choice.usage.sign_only;
+ }
+
+ for (int i = 0; i < choices.size(); ++i) {
+ if (choices.get(i).getId() == selectId) {
+ mUsage.setSelection(i);
+ break;
+ }
+ }
+
+ GregorianCalendar cal = new GregorianCalendar();
+ cal.setTime(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;
+ }
+
+ public void onClick(View v) {
+ final ViewGroup parent = (ViewGroup)getParent();
+ if (v == mDeleteButton) {
+ parent.removeView(this);
+ if (mEditorListener != null) {
+ mEditorListener.onDeleted(this);
+ }
+ }
+ }
+
+ public void setEditorListener(EditorListener listener) {
+ mEditorListener = listener;
+ }
+
+ private void setExpiryDate(GregorianCalendar date) {
+ mExpiryDate = date;
+ if (date == null) {
+ mExpiryDateButton.setText(R.string.none);
+ } else {
+ mExpiryDateButton.setText(DateFormat.getDateInstance().format(date.getTime()));
+ }
+ }
+
+ public GregorianCalendar getExpiryDate() {
+ return mExpiryDate;
+ }
+
+ public int getUsage() {
+ return ((Choice) mUsage.getSelectedItem()).getId();
+ }
+}
diff --git a/org_apg/src/org/apg/ui/widget/KeyServerEditor.java b/org_apg/src/org/apg/ui/widget/KeyServerEditor.java
new file mode 100644
index 000000000..3d8634c76
--- /dev/null
+++ b/org_apg/src/org/apg/ui/widget/KeyServerEditor.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apg.ui.widget;
+
+import org.apg.R;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class KeyServerEditor extends LinearLayout implements Editor, OnClickListener {
+ private EditorListener mEditorListener = null;
+
+ ImageButton mDeleteButton;
+ TextView mServer;
+
+ public KeyServerEditor(Context context) {
+ super(context);
+ }
+
+ public KeyServerEditor(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ setDrawingCacheEnabled(true);
+ setAlwaysDrawnWithCacheEnabled(true);
+
+ mServer = (TextView) findViewById(R.id.server);
+
+ mDeleteButton = (ImageButton) findViewById(R.id.delete);
+ mDeleteButton.setOnClickListener(this);
+
+ super.onFinishInflate();
+ }
+
+ public void setValue(String value) {
+ mServer.setText(value);
+ }
+
+ public String getValue() {
+ return mServer.getText().toString().trim();
+ }
+
+ public void onClick(View v) {
+ final ViewGroup parent = (ViewGroup)getParent();
+ if (v == mDeleteButton) {
+ parent.removeView(this);
+ if (mEditorListener != null) {
+ mEditorListener.onDeleted(this);
+ }
+ }
+ }
+
+ public void setEditorListener(EditorListener listener) {
+ mEditorListener = listener;
+ }
+}
diff --git a/org_apg/src/org/apg/ui/widget/SectionView.java b/org_apg/src/org/apg/ui/widget/SectionView.java
new file mode 100644
index 000000000..220699124
--- /dev/null
+++ b/org_apg/src/org/apg/ui/widget/SectionView.java
@@ -0,0 +1,335 @@
+/*
+ * 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.apg.ui.widget;
+
+import org.apg.Apg;
+import org.apg.Id;
+import org.apg.ui.widget.Editor.EditorListener;
+import org.apg.util.Choice;
+import org.spongycastle.openpgp.PGPException;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.apg.R;
+
+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.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidParameterException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.util.Vector;
+
+public class SectionView extends LinearLayout implements OnClickListener, EditorListener, Runnable {
+ private LayoutInflater mInflater;
+ private View mAdd;
+ private ViewGroup mEditors;
+ private TextView mTitle;
+ private int mType = 0;
+
+ private Choice 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(Apg.EXTRA_ERROR);
+ if (error != null) {
+ Toast.makeText(getContext(),
+ getContext().getString(R.string.errorMessage, 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 Id.type.user_id: {
+ mTitle.setText(R.string.section_userIds);
+ break;
+ }
+
+ case Id.type.key: {
+ mTitle.setText(R.string.section_keys);
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void onFinishInflate() {
+ mInflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ setDrawingCacheEnabled(true);
+ setAlwaysDrawnWithCacheEnabled(true);
+
+ mAdd = findViewById(R.id.header);
+ mAdd.setOnClickListener(this);
+
+ mEditors = (ViewGroup) findViewById(R.id.editors);
+ mTitle = (TextView) findViewById(R.id.title);
+
+ updateEditorsVisible();
+ super.onFinishInflate();
+ }
+
+ /** {@inheritDoc} */
+ public void onDeleted(Editor editor) {
+ this.updateEditorsVisible();
+ }
+
+ protected void updateEditorsVisible() {
+ final boolean hasChildren = mEditors.getChildCount() > 0;
+ mEditors.setVisibility(hasChildren ? View.VISIBLE : View.GONE);
+ }
+
+ /** {@inheritDoc} */
+ public void onClick(View v) {
+ switch (mType) {
+ case Id.type.user_id: {
+ UserIdEditor view =
+ (UserIdEditor) mInflater.inflate(R.layout.edit_key_user_id_item,
+ mEditors, false);
+ view.setEditorListener(this);
+ if (mEditors.getChildCount() == 0) {
+ view.setIsMainUserId(true);
+ }
+ mEditors.addView(view);
+ break;
+ }
+
+ case Id.type.key: {
+ AlertDialog.Builder dialog = new AlertDialog.Builder(getContext());
+
+ View view = mInflater.inflate(R.layout.create_key, null);
+ dialog.setView(view);
+ dialog.setTitle(R.string.title_createKey);
+ dialog.setMessage(R.string.keyCreationElGamalInfo);
+
+ boolean wouldBeMasterKey = (mEditors.getChildCount() == 0);
+
+ final Spinner algorithm = (Spinner) view.findViewById(R.id.algorithm);
+ Vector<Choice> choices = new Vector<Choice>();
+ choices.add(new Choice(Id.choice.algorithm.dsa,
+ getResources().getString(R.string.dsa)));
+ if (!wouldBeMasterKey) {
+ choices.add(new Choice(Id.choice.algorithm.elgamal,
+ getResources().getString(R.string.elgamal)));
+ }
+
+ choices.add(new Choice(Id.choice.algorithm.rsa,
+ getResources().getString(R.string.rsa)));
+
+ ArrayAdapter<Choice> adapter =
+ new ArrayAdapter<Choice>(getContext(),
+ android.R.layout.simple_spinner_item,
+ choices);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ algorithm.setAdapter(adapter);
+ // make RSA the default
+ for (int i = 0; i < choices.size(); ++i) {
+ if (choices.get(i).getId() == Id.choice.algorithm.rsa) {
+ algorithm.setSelection(i);
+ break;
+ }
+ }
+
+ final EditText keySize = (EditText) view.findViewById(R.id.size);
+
+ dialog.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface di, int id) {
+ di.dismiss();
+ try {
+ mNewKeySize = Integer.parseInt("" + keySize.getText());
+ } catch (NumberFormatException e) {
+ mNewKeySize = 0;
+ }
+
+ mNewKeyAlgorithmChoice = (Choice) algorithm.getSelectedItem();
+ createKey();
+ }
+ });
+
+ dialog.setCancelable(true);
+ dialog.setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface di, int id) {
+ di.dismiss();
+ }
+ });
+
+ dialog.create().show();
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ this.updateEditorsVisible();
+ }
+
+ public void setUserIds(Vector<String> list) {
+ if (mType != Id.type.user_id) {
+ return;
+ }
+
+ mEditors.removeAllViews();
+ for (String userId : list) {
+ UserIdEditor view =
+ (UserIdEditor) mInflater.inflate(R.layout.edit_key_user_id_item, mEditors, false);
+ view.setEditorListener(this);
+ view.setValue(userId);
+ if (mEditors.getChildCount() == 0) {
+ view.setIsMainUserId(true);
+ }
+ mEditors.addView(view);
+ }
+
+ this.updateEditorsVisible();
+ }
+
+ public void setKeys(Vector<PGPSecretKey> list) {
+ if (mType != Id.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(getContext().getString(R.string.progress_generating));
+ mProgressDialog.setCancelable(false);
+ mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
+ mProgressDialog.show();
+ mRunningThread = new Thread(this);
+ mRunningThread.start();
+ }
+
+ public void run() {
+ String error = null;
+ try {
+ PGPSecretKey masterKey = null;
+ String passPhrase;
+ if (mEditors.getChildCount() > 0) {
+ masterKey = ((KeyEditor) mEditors.getChildAt(0)).getValue();
+ passPhrase = Apg.getCachedPassPhrase(masterKey.getKeyID());
+ } else {
+ passPhrase = "";
+ }
+ mNewKey = Apg.createKey(getContext(),
+ mNewKeyAlgorithmChoice.getId(),
+ mNewKeySize, passPhrase,
+ masterKey);
+ } catch (NoSuchProviderException e) {
+ error = "" + e;
+ } catch (NoSuchAlgorithmException e) {
+ error = "" + e;
+ } catch (PGPException e) {
+ error = "" + e;
+ } catch (InvalidParameterException e) {
+ error = "" + e;
+ } catch (InvalidAlgorithmParameterException e) {
+ error = "" + e;
+ } catch (Apg.GeneralException e) {
+ error = "" + e;
+ }
+
+ Message message = new Message();
+ Bundle data = new Bundle();
+ data.putBoolean("closeProgressDialog", true);
+ if (error != null) {
+ data.putString(Apg.EXTRA_ERROR, error);
+ } else {
+ data.putBoolean("gotNewKey", true);
+ }
+ message.setData(data);
+ mHandler.sendMessage(message);
+ }
+}
diff --git a/org_apg/src/org/apg/ui/widget/UserIdEditor.java b/org_apg/src/org/apg/ui/widget/UserIdEditor.java
new file mode 100644
index 000000000..b154803cf
--- /dev/null
+++ b/org_apg/src/org/apg/ui/widget/UserIdEditor.java
@@ -0,0 +1,192 @@
+/*
+ * 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.apg.ui.widget;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apg.R;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.RadioButton;
+
+public class UserIdEditor extends LinearLayout implements Editor, OnClickListener {
+ private EditorListener mEditorListener = null;
+
+ private ImageButton mDeleteButton;
+ private RadioButton mIsMainUserId;
+ private EditText mName;
+ private EditText mEmail;
+ private EditText mComment;
+
+ 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.delete);
+ mDeleteButton.setOnClickListener(this);
+ mIsMainUserId = (RadioButton) findViewById(R.id.isMainUserId);
+ mIsMainUserId.setOnClickListener(this);
+
+ mName = (EditText) findViewById(R.id.name);
+ mEmail = (EditText) findViewById(R.id.email);
+ mComment = (EditText) findViewById(R.id.comment);
+
+ super.onFinishInflate();
+ }
+
+ public void setValue(String userId) {
+ mName.setText("");
+ mComment.setText("");
+ mEmail.setText("");
+
+ Pattern withComment = Pattern.compile("^(.*) [(](.*)[)] <(.*)>$");
+ Matcher matcher = withComment.matcher(userId);
+ if (matcher.matches()) {
+ mName.setText(matcher.group(1));
+ mComment.setText(matcher.group(2));
+ mEmail.setText(matcher.group(3));
+ return;
+ }
+
+ Pattern withoutComment = Pattern.compile("^(.*) <(.*)>$");
+ matcher = withoutComment.matcher(userId);
+ if (matcher.matches()) {
+ mName.setText(matcher.group(1));
+ mEmail.setText(matcher.group(2));
+ return;
+ }
+ }
+
+ public String getValue() throws NoNameException, NoEmailException, InvalidEmailException {
+ String name = ("" + mName.getText()).trim();
+ String email = ("" + mEmail.getText()).trim();
+ String comment = ("" + mComment.getText()).trim();
+
+ if (email.length() > 0) {
+ Matcher emailMatcher = EMAIL_PATTERN.matcher(email);
+ if (!emailMatcher.matches()) {
+ throw new InvalidEmailException(
+ getContext().getString(R.string.error_invalidEmail, email));
+ }
+ }
+
+ String userId = name;
+ if (comment.length() > 0) {
+ userId += " (" + comment + ")";
+ }
+ if (email.length() > 0) {
+ userId += " <" + email + ">";
+ }
+
+ if (userId.equals("")) {
+ // ok, empty one...
+ return userId;
+ }
+
+ // otherwise make sure that name and email exist
+ if (name.equals("")) {
+ throw new NoNameException("need a name");
+ }
+
+ if (email.equals("")) {
+ throw new NoEmailException("need an email");
+ }
+
+ return userId;
+ }
+
+ public void onClick(View v) {
+ final ViewGroup parent = (ViewGroup)getParent();
+ if (v == mDeleteButton) {
+ boolean wasMainUserId = mIsMainUserId.isChecked();
+ parent.removeView(this);
+ if (mEditorListener != null) {
+ mEditorListener.onDeleted(this);
+ }
+ if (wasMainUserId && parent.getChildCount() > 0) {
+ UserIdEditor editor = (UserIdEditor) parent.getChildAt(0);
+ editor.setIsMainUserId(true);
+ }
+ } else if (v == mIsMainUserId) {
+ for (int i = 0; i < parent.getChildCount(); ++i) {
+ UserIdEditor editor = (UserIdEditor) parent.getChildAt(i);
+ if (editor == this) {
+ editor.setIsMainUserId(true);
+ } else {
+ editor.setIsMainUserId(false);
+ }
+ }
+ }
+ }
+
+ public void setIsMainUserId(boolean value) {
+ mIsMainUserId.setChecked(value);
+ }
+
+ public boolean isMainUserId() {
+ return mIsMainUserId.isChecked();
+ }
+
+ public void setEditorListener(EditorListener listener) {
+ mEditorListener = listener;
+ }
+}