diff options
Diffstat (limited to 'org_apg/src/org/apg/ui')
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; + } +} |