aboutsummaryrefslogtreecommitdiffstats
path: root/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui
diff options
context:
space:
mode:
authorDominik Schürmann <dominik@dominikschuermann.de>2013-01-16 14:31:16 +0100
committerDominik Schürmann <dominik@dominikschuermann.de>2013-01-16 14:31:16 +0100
commit1feb948acf81532f82b36456080920543004b097 (patch)
treef22e51163db4303ad72b9205a3347aea8211c15e /OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui
parentdbbd8f6856086a9aa17b565080959fb77dc24cd9 (diff)
downloadopen-keychain-1feb948acf81532f82b36456080920543004b097.tar.gz
open-keychain-1feb948acf81532f82b36456080920543004b097.tar.bz2
open-keychain-1feb948acf81532f82b36456080920543004b097.zip
Renaming APG to OpenPGP Keychain
Diffstat (limited to 'OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui')
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/DecryptActivity.java946
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EditKeyActivity.java593
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EncryptActivity.java1105
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpActivity.java175
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentAbout.java106
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentHtml.java109
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java473
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java148
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListActivity.java314
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListFragment.java88
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicActivity.java121
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java220
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretActivity.java124
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretFragment.java146
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerQueryActivity.java353
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerUploadActivity.java146
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/MainActivity.java101
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/PreferencesActivity.java240
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/PreferencesKeyServerActivity.java162
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectPublicKeyActivity.java164
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectPublicKeyFragment.java232
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectSecretKeyActivity.java131
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectSecretKeyFragment.java154
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ShareActivity.java74
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ShareNfcBeamActivity.java245
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SignKeyActivity.java313
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/DeleteFileDialogFragment.java119
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java137
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java194
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/LookupUnknownKeyDialogFragment.java136
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java275
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/ProgressDialogFragment.java123
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java190
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/DashboardLayout.java186
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/Editor.java25
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/ExpandableListFragment.java530
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/ImportKeysListLoader.java170
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/IntegerListPreference.java95
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyEditor.java236
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyListAdapter.java264
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyServerEditor.java78
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SectionView.java327
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SelectKeyCursorAdapter.java142
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/UserIdEditor.java196
44 files changed, 10406 insertions, 0 deletions
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/DecryptActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/DecryptActivity.java
new file mode 100644
index 000000000..87b1204a4
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/DecryptActivity.java
@@ -0,0 +1,946 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * 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.sufficientlysecure.keychain.ui;
+
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.compatibility.ClipboardReflection;
+import org.sufficientlysecure.keychain.helper.FileHelper;
+import org.sufficientlysecure.keychain.helper.OtherHelper;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.helper.PgpMain;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.service.PassphraseCacheService;
+import org.sufficientlysecure.keychain.ui.dialog.DeleteFileDialogFragment;
+import org.sufficientlysecure.keychain.ui.dialog.FileDialogFragment;
+import org.sufficientlysecure.keychain.ui.dialog.LookupUnknownKeyDialogFragment;
+import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.app.ProgressDialog;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Messenger;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.animation.AnimationUtils;
+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.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.util.regex.Matcher;
+
+public class DecryptActivity extends SherlockFragmentActivity {
+
+ /* Intents */
+ // without permission
+ public static final String ACTION_DECRYPT = Constants.INTENT_PREFIX + "DECRYPT";
+ public static final String ACTION_DECRYPT_FILE = Constants.INTENT_PREFIX + "DECRYPT_FILE";
+
+ // with permission
+ public static final String ACTION_DECRYPT_AND_RETURN = Constants.INTENT_PREFIX
+ + "DECRYPT_AND_RETURN";
+ public static final String ACTION_DECRYPT_STREAM_AND_RETURN = Constants.INTENT_PREFIX
+ + "DECRYPT_STREAM_AND_RETURN";
+
+ /* EXTRA keys for input */
+ public static final String EXTRA_TEXT = "text";
+ public static final String EXTRA_DATA = "data";
+ public static final String EXTRA_REPLY_TO = "replyTo";
+ public static final String EXTRA_SUBJECT = "subject";
+ public static final String EXTRA_BINARY = "binary";
+
+ private long mSignatureKeyId = 0;
+
+ 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 boolean mDecryptEnabled = true;
+ private String mDecryptString = "";
+ private boolean mReplyEnabled = true;
+ private String mReplyString = "";
+
+ 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[] mDataBytes = null;
+ private boolean mReturnBinary = false;
+
+ private long mUnknownSignatureKeyId = 0;
+
+ private long mSecretKeyId = Id.key.none;
+
+ private FileDialogFragment mFileDialog;
+
+ private boolean mLookupUnknownKey = true;
+
+ private boolean mDecryptImmediately = false;
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+
+ if (mDecryptEnabled) {
+ menu.add(1, Id.menu.option.decrypt, 0, mDecryptString).setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ }
+ if (mReplyEnabled) {
+ menu.add(1, Id.menu.option.reply, 1, mReplyString).setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go home
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+
+ case Id.menu.option.decrypt: {
+ decryptClicked();
+
+ return true;
+ }
+ case Id.menu.option.reply: {
+ replyClicked();
+
+ return true;
+ }
+
+ default: {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+ }
+
+ private void initView() {
+ 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);
+ 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) {
+ FileHelper.openFile(DecryptActivity.this, mFilename.getText().toString(), "*/*",
+ Id.request.filename);
+ }
+ });
+
+ mDeleteAfter = (CheckBox) findViewById(R.id.deleteAfterDecryption);
+
+ // default: message source
+ mSource.setInAnimation(null);
+ mSource.setOutAnimation(null);
+ while (mSource.getCurrentView().getId() != R.id.sourceMessage) {
+ mSource.showNext();
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // check permissions for intent actions without user interaction
+ String[] restrictedActions = new String[] { ACTION_DECRYPT_AND_RETURN };
+ OtherHelper.checkPackagePermissionForActions(this, this.getCallingPackage(),
+ Constants.PERMISSION_ACCESS_API, getIntent().getAction(), restrictedActions);
+
+ setContentView(R.layout.decrypt);
+
+ // set actionbar without home button if called from another app
+ OtherHelper.setActionBarBackButton(this);
+
+ initView();
+
+ // Handle intent actions
+ handleActions(getIntent());
+
+ if (mSource.getCurrentView().getId() == R.id.sourceMessage
+ && mMessage.getText().length() == 0) {
+
+ CharSequence clipboardText = ClipboardReflection.getClipboardText(this);
+
+ String data = "";
+ if (clipboardText != null) {
+ Matcher matcher = PgpMain.PGP_MESSAGE.matcher(clipboardText);
+ if (!matcher.matches()) {
+ matcher = PgpMain.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 = ProviderHelper.getPGPPublicKeyRingByKeyId(
+ DecryptActivity.this, mSignatureKeyId);
+ if (key != null) {
+ Intent intent = new Intent(DecryptActivity.this, KeyServerQueryActivity.class);
+ intent.setAction(KeyServerQueryActivity.ACTION_LOOK_UP_KEY_ID);
+ intent.putExtra(KeyServerQueryActivity.EXTRA_KEY_ID, mSignatureKeyId);
+ startActivity(intent);
+ }
+ }
+ });
+
+ mReplyEnabled = false;
+
+ // build new actionbar
+ invalidateOptionsMenu();
+
+ 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 (mDecryptImmediately
+ || (mSource.getCurrentView().getId() == R.id.sourceMessage && (mMessage.getText()
+ .length() > 0 || mDataBytes != null || mContentUri != null))) {
+ decryptClicked();
+ }
+ }
+
+ /**
+ * Handles all actions with this intent
+ *
+ * @param intent
+ */
+ private void handleActions(Intent intent) {
+ String action = intent.getAction();
+ Bundle extras = intent.getExtras();
+ String type = intent.getType();
+ Uri uri = intent.getData();
+
+ if (extras == null) {
+ extras = new Bundle();
+ }
+
+ /*
+ * Android's Action
+ */
+ if (Intent.ACTION_SEND.equals(action) && type != null) {
+ // When sending to APG Encrypt via share menu
+ if ("text/plain".equals(type)) {
+ // Plain text
+ String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
+ if (sharedText != null) {
+ // handle like normal text decryption, override action and extras to later
+ // execute ACTION_DECRYPT in main actions
+ extras.putString(EXTRA_TEXT, sharedText);
+ action = ACTION_DECRYPT;
+ }
+ } else {
+ // Binary via content provider (could also be files)
+ // override uri to get stream from send
+ uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
+ action = ACTION_DECRYPT_FILE;
+ }
+ } else if (Intent.ACTION_VIEW.equals(action)) {
+ // Android's Action when opening file associated to APG (see AndroidManifest.xml)
+
+ // override action
+ action = ACTION_DECRYPT_FILE;
+
+ // EVERYTHING ELSE IS OLD CODE
+ // This gets the Uri, where an inputStream can be opened from
+ // mContentUri = intent.getData();
+
+ // TODO: old implementation of ACTION_VIEW. Is this used in K9?
+ // 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
+ // }
+
+ // same as ACTION_DECRYPT_FILE but decrypt it immediately
+ // handleActionDecryptFile(intent);
+ // mDecryptImmediately = true;
+ }
+
+ /**
+ * Main Actions
+ */
+ if (ACTION_DECRYPT.equals(action)) {
+ mDataBytes = extras.getByteArray(EXTRA_DATA);
+ String textData = null;
+ if (mDataBytes == null) {
+ Log.d(Constants.TAG, "EXTRA_DATA was null");
+ textData = extras.getString(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 = PgpMain.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 = PgpMain.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);
+
+ mDecryptString = getString(R.string.btn_verify);
+ // build new action bar
+ invalidateOptionsMenu();
+ } else {
+ Log.d(Constants.TAG, "Nothing matched!");
+ }
+ }
+ }
+ mReplyTo = extras.getString(EXTRA_REPLY_TO);
+ mSubject = extras.getString(EXTRA_SUBJECT);
+ } else if (ACTION_DECRYPT_FILE.equals(action)) {
+ // get file path from uri
+ String path = FileHelper.getPath(this, uri);
+
+ if (path != null) {
+ mInputFilename = path;
+ mFilename.setText(mInputFilename);
+ guessOutputFilename();
+ mSource.setInAnimation(null);
+ mSource.setOutAnimation(null);
+ while (mSource.getCurrentView().getId() != R.id.sourceFile) {
+ mSource.showNext();
+ }
+ } else {
+ Log.e(Constants.TAG,
+ "Direct binary data without actual file in filesystem is not supported. This is only supported by ACTION_DECRYPT_STREAM_AND_RETURN.");
+ Toast.makeText(this, R.string.error_onlyFilesAreSupported, Toast.LENGTH_LONG)
+ .show();
+ // end activity
+ finish();
+ }
+ } else if (ACTION_DECRYPT_AND_RETURN.equals(action)) {
+ mReturnBinary = extras.getBoolean(EXTRA_BINARY, false);
+
+ if (mContentUri == null) {
+ mDataBytes = extras.getByteArray(EXTRA_DATA);
+ String data = extras.getString(EXTRA_TEXT);
+ if (data != null) {
+ Matcher matcher = PgpMain.PGP_MESSAGE.matcher(data);
+ if (matcher.matches()) {
+ data = matcher.group(1);
+ // replace non breakable spaces
+ data = data.replaceAll("\\xa0", " ");
+ mMessage.setText(data);
+ } else {
+ matcher = PgpMain.PGP_SIGNED_MESSAGE.matcher(data);
+ if (matcher.matches()) {
+ data = matcher.group(1);
+ // replace non breakable spaces
+ data = data.replaceAll("\\xa0", " ");
+ mMessage.setText(data);
+ mDecryptString = getString(R.string.btn_verify);
+
+ // build new action bar
+ invalidateOptionsMenu();
+ }
+ }
+ }
+ }
+ mReturnResult = true;
+ } else if (ACTION_DECRYPT_STREAM_AND_RETURN.equals(action)) {
+ // TODO: Implement decrypt stream
+ }
+ }
+
+ 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);
+ mDecryptString = getString(R.string.btn_decrypt);
+
+ // build new action bar
+ invalidateOptionsMenu();
+ break;
+ }
+
+ case R.id.sourceMessage: {
+ mSourceLabel.setText(R.string.label_message);
+ mDecryptString = getString(R.string.btn_decrypt);
+
+ // build new action bar
+ invalidateOptionsMenu();
+ 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 = PgpMain.PGP_SIGNED_MESSAGE.matcher(messageData);
+ if (matcher.matches()) {
+ mSignedOnly = true;
+ decryptStart();
+ return;
+ }
+ }
+
+ // else treat it as an decrypted message/file
+ mSignedOnly = false;
+
+ getDecryptionKeyFromInputStream();
+
+ // if we need a symmetric passphrase or a passphrase to use a secret key ask for it
+ if (mSecretKeyId == Id.key.symmetric
+ || PassphraseCacheService.getCachedPassphrase(this, mSecretKeyId) == null) {
+ showPassphraseDialog();
+ } else {
+ if (mDecryptTarget == Id.target.file) {
+ askForOutputFilename();
+ } else {
+ decryptStart();
+ }
+ }
+ }
+
+ /**
+ * Shows passphrase dialog to cache a new passphrase the user enters for using it later for
+ * encryption. Based on mSecretKeyId it asks for a passphrase to open a private key or it asks
+ * for a symmetric passphrase
+ */
+ private void showPassphraseDialog() {
+ // Message is received after passphrase is cached
+ Handler returnHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) {
+ if (mDecryptTarget == Id.target.file) {
+ askForOutputFilename();
+ } else {
+ decryptStart();
+ }
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(returnHandler);
+
+ try {
+ PassphraseDialogFragment passphraseDialog = PassphraseDialogFragment.newInstance(this,
+ messenger, mSecretKeyId);
+
+ passphraseDialog.show(getSupportFragmentManager(), "passphraseDialog");
+ } catch (PgpMain.PgpGeneralException e) {
+ Log.d(Constants.TAG, "No passphrase for this secret key, encrypt directly!");
+ // send message to handler to start encryption directly
+ returnHandler.sendEmptyMessage(PassphraseDialogFragment.MESSAGE_OKAY);
+ }
+ }
+
+ /**
+ * TODO: Rework function, remove global variables
+ */
+ private void getDecryptionKeyFromInputStream() {
+ InputStream inStream = null;
+ if (mContentUri != null) {
+ try {
+ inStream = getContentResolver().openInputStream(mContentUri);
+ } catch (FileNotFoundException e) {
+ Log.e(Constants.TAG, "File not found!", e);
+ Toast.makeText(this, getString(R.string.error_fileNotFound, e.getMessage()),
+ Toast.LENGTH_SHORT).show();
+ }
+ } else if (mDecryptTarget == Id.target.file) {
+ // check if storage is ready
+ if (!FileHelper.isStorageMounted(mInputFilename)) {
+ Toast.makeText(this, getString(R.string.error_externalStorageNotReady),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ try {
+ inStream = new FileInputStream(mInputFilename);
+ } catch (FileNotFoundException e) {
+ Log.e(Constants.TAG, "File not found!", e);
+ Toast.makeText(this, getString(R.string.error_fileNotFound, e.getMessage()),
+ Toast.LENGTH_SHORT).show();
+ }
+ } else {
+ if (mDataBytes != null) {
+ inStream = new ByteArrayInputStream(mDataBytes);
+ } else {
+ inStream = new ByteArrayInputStream(mMessage.getText().toString().getBytes());
+ }
+ }
+
+ // get decryption key for this inStream
+ try {
+ try {
+ mSecretKeyId = PgpMain.getDecryptionKeyId(this, inStream);
+ if (mSecretKeyId == Id.key.none) {
+ throw new PgpMain.PgpGeneralException(
+ getString(R.string.error_noSecretKeyFound));
+ }
+ mAssumeSymmetricEncryption = false;
+ } catch (PgpMain.NoAsymmetricEncryptionException e) {
+ mSecretKeyId = Id.key.symmetric;
+ if (!PgpMain.hasSymmetricEncryption(this, inStream)) {
+ throw new PgpMain.PgpGeneralException(
+ getString(R.string.error_noKnownEncryptionFound));
+ }
+ mAssumeSymmetricEncryption = true;
+ }
+ } catch (Exception e) {
+ Toast.makeText(this, getString(R.string.errorMessage, e.getMessage()),
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void replyClicked() {
+ Intent intent = new Intent(this, EncryptActivity.class);
+ intent.setAction(EncryptActivity.ACTION_ENCRYPT);
+ String data = mMessage.getText().toString();
+ data = data.replaceAll("(?m)^", "> ");
+ data = "\n\n" + data;
+ intent.putExtra(EncryptActivity.EXTRA_TEXT, data);
+ intent.putExtra(EncryptActivity.EXTRA_SUBJECT, "Re: " + mSubject);
+ intent.putExtra(EncryptActivity.EXTRA_SEND_TO, mReplyTo);
+ intent.putExtra(EncryptActivity.EXTRA_SIGNATURE_KEY_ID, mSecretKeyId);
+ intent.putExtra(EncryptActivity.EXTRA_ENCRYPTION_KEY_IDS, new long[] { mSignatureKeyId });
+ startActivity(intent);
+ }
+
+ private void askForOutputFilename() {
+ // Message is received after passphrase is cached
+ Handler returnHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == FileDialogFragment.MESSAGE_OKAY) {
+ Bundle data = message.getData();
+ mOutputFilename = data.getString(FileDialogFragment.MESSAGE_DATA_FILENAME);
+ decryptStart();
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(returnHandler);
+
+ mFileDialog = FileDialogFragment.newInstance(messenger,
+ getString(R.string.title_decryptToFile),
+ getString(R.string.specifyFileToDecryptTo), mOutputFilename, null,
+ Id.request.output_filename);
+
+ mFileDialog.show(getSupportFragmentManager(), "fileDialog");
+ }
+
+ private void lookupUnknownKey(long unknownKeyId) {
+ // Message is received after passphrase is cached
+ Handler returnHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == LookupUnknownKeyDialogFragment.MESSAGE_OKAY) {
+ // the result is handled by onActivityResult() as LookupUnknownKeyDialogFragment
+ // starts a new Intent which then returns data
+ } else if (message.what == LookupUnknownKeyDialogFragment.MESSAGE_CANCEL) {
+ // decrypt again, but don't lookup unknown keys!
+ mLookupUnknownKey = false;
+ decryptStart();
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(returnHandler);
+
+ LookupUnknownKeyDialogFragment lookupKeyDialog = LookupUnknownKeyDialogFragment
+ .newInstance(messenger, unknownKeyId);
+
+ lookupKeyDialog.show(getSupportFragmentManager(), "unknownKeyDialog");
+ }
+
+ private void decryptStart() {
+ Log.d(Constants.TAG, "decryptStart");
+
+ // Send all information needed to service to decrypt in other thread
+ Intent intent = new Intent(this, KeychainIntentService.class);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_DECRYPT_VERIFY);
+
+ // choose action based on input: decrypt stream, file or bytes
+ if (mContentUri != null) {
+ data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_STREAM);
+
+ data.putParcelable(KeychainIntentService.ENCRYPT_PROVIDER_URI, mContentUri);
+ } else if (mDecryptTarget == Id.target.file) {
+ data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_FILE);
+
+ Log.d(Constants.TAG, "mInputFilename=" + mInputFilename + ", mOutputFilename="
+ + mOutputFilename);
+
+ data.putString(KeychainIntentService.ENCRYPT_INPUT_FILE, mInputFilename);
+ data.putString(KeychainIntentService.ENCRYPT_OUTPUT_FILE, mOutputFilename);
+ } else {
+ data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_BYTES);
+
+ if (mDataBytes != null) {
+ data.putByteArray(KeychainIntentService.DECRYPT_CIPHERTEXT_BYTES, mDataBytes);
+ } else {
+ String message = mMessage.getText().toString();
+ data.putByteArray(KeychainIntentService.DECRYPT_CIPHERTEXT_BYTES, message.getBytes());
+ }
+ }
+
+ data.putLong(KeychainIntentService.ENCRYPT_SECRET_KEY_ID, mSecretKeyId);
+
+ data.putBoolean(KeychainIntentService.DECRYPT_SIGNED_ONLY, mSignedOnly);
+ data.putBoolean(KeychainIntentService.DECRYPT_LOOKUP_UNKNOWN_KEY, mLookupUnknownKey);
+ data.putBoolean(KeychainIntentService.DECRYPT_RETURN_BYTES, mReturnBinary);
+ data.putBoolean(KeychainIntentService.DECRYPT_ASSUME_SYMMETRIC, mAssumeSymmetricEncryption);
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after encrypting is done in ApgService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
+ R.string.progress_decrypting, ProgressDialog.STYLE_HORIZONTAL) {
+ public void handleMessage(Message message) {
+ // handle messages by standard ApgHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ // get returned data bundle
+ Bundle returnData = message.getData();
+
+ // if key is unknown show lookup dialog
+ if (returnData.getBoolean(KeychainIntentService.RESULT_SIGNATURE_LOOKUP_KEY)
+ && mLookupUnknownKey) {
+ mUnknownSignatureKeyId = returnData
+ .getLong(KeychainIntentService.RESULT_SIGNATURE_KEY_ID);
+ lookupUnknownKey(mUnknownSignatureKeyId);
+ return;
+ }
+
+ mSignatureKeyId = 0;
+ mSignatureLayout.setVisibility(View.GONE);
+ mReplyEnabled = false;
+
+ // build new action bar
+ invalidateOptionsMenu();
+
+ Toast.makeText(DecryptActivity.this, R.string.decryptionSuccessful,
+ Toast.LENGTH_SHORT).show();
+ if (mReturnResult) {
+ Intent intent = new Intent();
+ intent.putExtras(returnData);
+ setResult(RESULT_OK, intent);
+ finish();
+ return;
+ }
+
+ switch (mDecryptTarget) {
+ case Id.target.message:
+ String decryptedMessage = returnData
+ .getString(KeychainIntentService.RESULT_DECRYPTED_STRING);
+ mMessage.setText(decryptedMessage);
+ mMessage.setHorizontallyScrolling(false);
+ mReplyEnabled = false;
+
+ // build new action bar
+ invalidateOptionsMenu();
+ break;
+
+ case Id.target.file:
+ if (mDeleteAfter.isChecked()) {
+ // Create and show dialog to delete original file
+ DeleteFileDialogFragment deleteFileDialog = DeleteFileDialogFragment
+ .newInstance(mInputFilename);
+ deleteFileDialog.show(getSupportFragmentManager(), "deleteDialog");
+ }
+ break;
+
+ default:
+ // shouldn't happen
+ break;
+
+ }
+
+ if (returnData.getBoolean(KeychainIntentService.RESULT_SIGNATURE)) {
+ String userId = returnData
+ .getString(KeychainIntentService.RESULT_SIGNATURE_USER_ID);
+ mSignatureKeyId = returnData
+ .getLong(KeychainIntentService.RESULT_SIGNATURE_KEY_ID);
+ mUserIdRest
+ .setText("id: " + PgpHelper.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 (returnData.getBoolean(KeychainIntentService.RESULT_SIGNATURE_SUCCESS)) {
+ mSignatureStatusImage.setImageResource(R.drawable.overlay_ok);
+ } else if (returnData.getBoolean(KeychainIntentService.RESULT_SIGNATURE_UNKNOWN)) {
+ mSignatureStatusImage.setImageResource(R.drawable.overlay_error);
+ Toast.makeText(DecryptActivity.this,
+ R.string.unknownSignatureKeyTouchToLookUp, Toast.LENGTH_LONG)
+ .show();
+ } else {
+ mSignatureStatusImage.setImageResource(R.drawable.overlay_error);
+ }
+ mSignatureLayout.setVisibility(View.VISIBLE);
+ }
+ }
+ };
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // show progress dialog
+ saveHandler.showProgressDialog(this);
+
+ // start service with intent
+ startService(intent);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.request.filename: {
+ if (resultCode == RESULT_OK && data != null) {
+ try {
+ String path = FileHelper.getPath(this, data.getData());
+ Log.d(Constants.TAG, "path=" + path);
+
+ mFilename.setText(path);
+ } catch (NullPointerException e) {
+ Log.e(Constants.TAG, "Nullpointer while retrieving path!");
+ }
+ }
+ return;
+ }
+
+ case Id.request.output_filename: {
+ if (resultCode == RESULT_OK && data != null) {
+ try {
+ String path = data.getData().getPath();
+ Log.d(Constants.TAG, "path=" + path);
+
+ mFileDialog.setFilename(path);
+ } catch (NullPointerException e) {
+ Log.e(Constants.TAG, "Nullpointer while retrieving path!");
+ }
+ }
+ return;
+ }
+
+ // this request is returned after LookupUnknownKeyDialogFragment started
+ // KeyServerQueryActivity and user looked uo key
+ case Id.request.look_up_key_id: {
+ Log.d(Constants.TAG, "Returning from Lookup Key...");
+ // decrypt again without lookup
+ mLookupUnknownKey = false;
+ decryptStart();
+ return;
+ }
+
+ default: {
+ break;
+ }
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EditKeyActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EditKeyActivity.java
new file mode 100644
index 000000000..73a1234c4
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EditKeyActivity.java
@@ -0,0 +1,593 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * 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.sufficientlysecure.keychain.ui;
+
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.OtherHelper;
+import org.sufficientlysecure.keychain.helper.PgpConversionHelper;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.helper.PgpMain;
+import org.sufficientlysecure.keychain.helper.PgpMain.PgpGeneralException;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.ui.dialog.SetPassphraseDialogFragment;
+import org.sufficientlysecure.keychain.ui.widget.KeyEditor;
+import org.sufficientlysecure.keychain.ui.widget.SectionView;
+import org.sufficientlysecure.keychain.ui.widget.UserIdEditor;
+import org.sufficientlysecure.keychain.util.IterableIterator;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Messenger;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.LinearLayout;
+import android.widget.Toast;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Vector;
+
+public class EditKeyActivity extends SherlockFragmentActivity {
+
+ // possible intent actions for this activity
+ public static final String ACTION_CREATE_KEY = Constants.INTENT_PREFIX + "CREATE_KEY";
+ public static final String ACTION_EDIT_KEY = Constants.INTENT_PREFIX + "EDIT_KEY";
+
+ // possible extra keys
+ public static final String EXTRA_USER_IDS = "userIds";
+ public static final String EXTRA_NO_PASSPHRASE = "noPassphrase";
+ public static final String EXTRA_GENERATE_DEFAULT_KEYS = "generateDefaultKeys";
+ public static final String EXTRA_KEY_ID = "keyId";
+
+ // results when saving key
+ public static final String RESULT_EXTRA_MASTER_KEY_ID = "masterKeyId";
+ public static final String RESULT_EXTRA_USER_ID = "userId";
+
+ private ActionBar mActionBar;
+
+ private PGPSecretKeyRing mKeyRing = null;
+
+ private SectionView mUserIdsView;
+ private SectionView mKeysView;
+
+ private String mCurrentPassPhrase = null;
+ private String mNewPassPhrase = null;
+
+ private Button mChangePassPhrase;
+
+ private CheckBox mNoPassphrase;
+
+ Vector<String> mUserIds;
+ Vector<PGPSecretKey> mKeys;
+ Vector<Integer> mKeysUsages;
+
+ // will be set to false to build layout later in handler
+ private boolean mBuildLayout = true;
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(1, Id.menu.option.cancel, 0, R.string.btn_doNotSave).setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ menu.add(1, Id.menu.option.save, 1, R.string.btn_save).setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go home
+ Intent intent = new Intent(this, KeyListSecretActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+
+ case Id.menu.option.save:
+ saveClicked();
+ return true;
+
+ case Id.menu.option.cancel:
+ cancelClicked();
+ return true;
+
+ default:
+ break;
+
+ }
+ return false;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // check permissions for intent actions without user interaction
+ String[] restrictedActions = new String[] { ACTION_CREATE_KEY };
+ OtherHelper.checkPackagePermissionForActions(this, this.getCallingPackage(),
+ Constants.PERMISSION_ACCESS_API, getIntent().getAction(), restrictedActions);
+
+ setContentView(R.layout.edit_key);
+
+ mActionBar = getSupportActionBar();
+ mActionBar.setDisplayShowTitleEnabled(true);
+
+ // set actionbar without home button if called from another app
+ OtherHelper.setActionBarBackButton(this);
+
+ // find views
+ mChangePassPhrase = (Button) findViewById(R.id.edit_key_btn_change_pass_phrase);
+ mNoPassphrase = (CheckBox) findViewById(R.id.edit_key_no_passphrase);
+
+ mUserIds = new Vector<String>();
+ mKeys = new Vector<PGPSecretKey>();
+ mKeysUsages = new Vector<Integer>();
+
+ // Catch Intents opened from other apps
+ Intent intent = getIntent();
+ String action = intent.getAction();
+ if (ACTION_CREATE_KEY.equals(action)) {
+ handleActionCreateKey(intent);
+ } else if (ACTION_EDIT_KEY.equals(action)) {
+ handleActionEditKey(intent);
+ }
+
+ mChangePassPhrase.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ showSetPassphraseDialog();
+ }
+ });
+
+ // disable passphrase when no passphrase checkobox is checked!
+ mNoPassphrase.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ if (isChecked) {
+ // remove passphrase
+ mNewPassPhrase = null;
+
+ mChangePassPhrase.setVisibility(View.GONE);
+ } else {
+ mChangePassPhrase.setVisibility(View.VISIBLE);
+ }
+ }
+ });
+
+ if (mBuildLayout) {
+ buildLayout();
+ }
+ }
+
+ /**
+ * Handle intent action to create new key
+ *
+ * @param intent
+ */
+ private void handleActionCreateKey(Intent intent) {
+ Bundle extras = intent.getExtras();
+
+ mActionBar.setTitle(R.string.title_createKey);
+
+ mCurrentPassPhrase = "";
+
+ if (extras != null) {
+ // if userId is given, prefill the fields
+ if (extras.containsKey(EXTRA_USER_IDS)) {
+ Log.d(Constants.TAG, "UserIds are given!");
+ mUserIds.add(extras.getString(EXTRA_USER_IDS));
+ }
+
+ // if no passphrase is given
+ if (extras.containsKey(EXTRA_NO_PASSPHRASE)) {
+ boolean noPassphrase = extras.getBoolean(EXTRA_NO_PASSPHRASE);
+ if (noPassphrase) {
+ // check "no passphrase" checkbox and remove button
+ mNoPassphrase.setChecked(true);
+ mChangePassPhrase.setVisibility(View.GONE);
+ }
+ }
+
+ // generate key
+ if (extras.containsKey(EXTRA_GENERATE_DEFAULT_KEYS)) {
+ boolean generateDefaultKeys = extras.getBoolean(EXTRA_GENERATE_DEFAULT_KEYS);
+ if (generateDefaultKeys) {
+
+ // build layout in handler after generating keys not directly in onCreate
+ mBuildLayout = false;
+
+ // Send all information needed to service generate keys in other thread
+ Intent serviceIntent = new Intent(this, KeychainIntentService.class);
+ serviceIntent.putExtra(KeychainIntentService.EXTRA_ACTION,
+ KeychainIntentService.ACTION_GENERATE_DEFAULT_RSA_KEYS);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+ data.putString(KeychainIntentService.GENERATE_KEY_SYMMETRIC_PASSPHRASE,
+ mCurrentPassPhrase);
+
+ serviceIntent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after generating is done in ApgService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
+ R.string.progress_generating, ProgressDialog.STYLE_SPINNER) {
+ public void handleMessage(Message message) {
+ // handle messages by standard ApgHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ // get new key from data bundle returned from service
+ Bundle data = message.getData();
+ PGPSecretKeyRing masterKeyRing = (PGPSecretKeyRing) PgpConversionHelper
+ .BytesToPGPKeyRing(data
+ .getByteArray(KeychainIntentService.RESULT_NEW_KEY));
+ PGPSecretKeyRing subKeyRing = (PGPSecretKeyRing) PgpConversionHelper
+ .BytesToPGPKeyRing(data
+ .getByteArray(KeychainIntentService.RESULT_NEW_KEY2));
+
+ // add master key
+ @SuppressWarnings("unchecked")
+ Iterator<PGPSecretKey> masterIt = masterKeyRing.getSecretKeys();
+ mKeys.add(masterIt.next());
+ mKeysUsages.add(Id.choice.usage.sign_only);
+
+ // add sub key
+ @SuppressWarnings("unchecked")
+ Iterator<PGPSecretKey> subIt = subKeyRing.getSecretKeys();
+ subIt.next(); // masterkey
+ mKeys.add(subIt.next());
+ mKeysUsages.add(Id.choice.usage.encrypt_only);
+
+ buildLayout();
+ }
+ };
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ serviceIntent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ saveHandler.showProgressDialog(this);
+
+ // start service with intent
+ startService(serviceIntent);
+ }
+ }
+ }
+ }
+
+ /**
+ * Handle intent action to edit existing key
+ *
+ * @param intent
+ */
+ @SuppressWarnings("unchecked")
+ private void handleActionEditKey(Intent intent) {
+ Bundle extras = intent.getExtras();
+
+ mActionBar.setTitle(R.string.title_editKey);
+
+ mCurrentPassPhrase = PgpMain.getEditPassPhrase();
+ if (mCurrentPassPhrase == null) {
+ mCurrentPassPhrase = "";
+ }
+
+ if (mCurrentPassPhrase.equals("")) {
+ // check "no passphrase" checkbox and remove button
+ mNoPassphrase.setChecked(true);
+ mChangePassPhrase.setVisibility(View.GONE);
+ }
+
+ if (extras != null) {
+ if (extras.containsKey(EXTRA_KEY_ID)) {
+ long keyId = extras.getLong(EXTRA_KEY_ID);
+
+ if (keyId != 0) {
+ PGPSecretKey masterKey = null;
+ mKeyRing = ProviderHelper.getPGPSecretKeyRingByMasterKeyId(this, keyId);
+ if (mKeyRing != null) {
+ masterKey = PgpHelper.getMasterKey(mKeyRing);
+ for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(
+ mKeyRing.getSecretKeys())) {
+ mKeys.add(key);
+ mKeysUsages.add(-1); // get usage when view is created
+ }
+ }
+ if (masterKey != null) {
+ for (String userId : new IterableIterator<String>(masterKey.getUserIDs())) {
+ Log.d(Constants.TAG, "Added userId " + userId);
+ mUserIds.add(userId);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Shows the dialog to set a new passphrase
+ */
+ private void showSetPassphraseDialog() {
+ // Message is received after passphrase is cached
+ Handler returnHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == SetPassphraseDialogFragment.MESSAGE_OKAY) {
+ Bundle data = message.getData();
+
+ // set new returned passphrase!
+ mNewPassPhrase = data
+ .getString(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE);
+
+ updatePassPhraseButtonText();
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(returnHandler);
+
+ // set title based on isPassphraseSet()
+ int title = -1;
+ if (isPassphraseSet()) {
+ title = R.string.title_changePassPhrase;
+ } else {
+ title = R.string.title_setPassPhrase;
+ }
+
+ SetPassphraseDialogFragment setPassphraseDialog = SetPassphraseDialogFragment.newInstance(
+ messenger, title);
+
+ setPassphraseDialog.show(getSupportFragmentManager(), "setPassphraseDialog");
+ }
+
+ /**
+ * Build layout based on mUserId, mKeys and mKeysUsages Vectors. It creates Views for every user
+ * id and key.
+ */
+ private void buildLayout() {
+ // Build layout based on given userIds and keys
+ LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ LinearLayout container = (LinearLayout) findViewById(R.id.edit_key_container);
+ mUserIdsView = (SectionView) inflater.inflate(R.layout.edit_key_section, container, false);
+ mUserIdsView.setType(Id.type.user_id);
+ mUserIdsView.setUserIds(mUserIds);
+ container.addView(mUserIdsView);
+ mKeysView = (SectionView) inflater.inflate(R.layout.edit_key_section, container, false);
+ mKeysView.setType(Id.type.key);
+ mKeysView.setKeys(mKeys, mKeysUsages);
+ container.addView(mKeysView);
+
+ updatePassPhraseButtonText();
+ }
+
+ private long getMasterKeyId() {
+ if (mKeysView.getEditors().getChildCount() == 0) {
+ return 0;
+ }
+ return ((KeyEditor) mKeysView.getEditors().getChildAt(0)).getValue().getKeyID();
+ }
+
+ public boolean isPassphraseSet() {
+ if (mNoPassphrase.isChecked()) {
+ return true;
+ } else if ((!mCurrentPassPhrase.equals(""))
+ || (mNewPassPhrase != null && !mNewPassPhrase.equals(""))) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private void saveClicked() {
+ try {
+ if (!isPassphraseSet()) {
+ throw new PgpMain.PgpGeneralException(this.getString(R.string.setAPassPhrase));
+ }
+
+ // Send all information needed to service to edit key in other thread
+ Intent intent = new Intent(this, KeychainIntentService.class);
+
+ intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_SAVE_KEYRING);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+ data.putString(KeychainIntentService.SAVE_KEYRING_CURRENT_PASSPHRASE, mCurrentPassPhrase);
+ data.putString(KeychainIntentService.SAVE_KEYRING_NEW_PASSPHRASE, mNewPassPhrase);
+ data.putStringArrayList(KeychainIntentService.SAVE_KEYRING_USER_IDS,
+ getUserIds(mUserIdsView));
+ ArrayList<PGPSecretKey> keys = getKeys(mKeysView);
+ data.putByteArray(KeychainIntentService.SAVE_KEYRING_KEYS,
+ PgpConversionHelper.PGPSecretKeyArrayListToBytes(keys));
+ data.putIntegerArrayList(KeychainIntentService.SAVE_KEYRING_KEYS_USAGES,
+ getKeysUsages(mKeysView));
+ data.putLong(KeychainIntentService.SAVE_KEYRING_MASTER_KEY_ID, getMasterKeyId());
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after saving is done in ApgService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
+ R.string.progress_saving, ProgressDialog.STYLE_HORIZONTAL) {
+ public void handleMessage(Message message) {
+ // handle messages by standard ApgHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ Intent data = new Intent();
+ data.putExtra(RESULT_EXTRA_MASTER_KEY_ID, getMasterKeyId());
+ ArrayList<String> userIds = null;
+ try {
+ userIds = getUserIds(mUserIdsView);
+ } catch (PgpGeneralException e) {
+ Log.e(Constants.TAG, "exception while getting user ids", e);
+ }
+ data.putExtra(RESULT_EXTRA_USER_ID, userIds.get(0));
+ setResult(RESULT_OK, data);
+ finish();
+ }
+ };
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ saveHandler.showProgressDialog(this);
+
+ // start service with intent
+ startService(intent);
+ } catch (PgpMain.PgpGeneralException e) {
+ Toast.makeText(this, getString(R.string.errorMessage, e.getMessage()),
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void cancelClicked() {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+
+ /**
+ * Returns user ids from the SectionView
+ *
+ * @param userIdsView
+ * @return
+ */
+ private ArrayList<String> getUserIds(SectionView userIdsView)
+ throws PgpMain.PgpGeneralException {
+ ArrayList<String> userIds = new ArrayList<String>();
+
+ ViewGroup userIdEditors = userIdsView.getEditors();
+
+ boolean gotMainUserId = false;
+ for (int i = 0; i < userIdEditors.getChildCount(); ++i) {
+ UserIdEditor editor = (UserIdEditor) userIdEditors.getChildAt(i);
+ String userId = null;
+ try {
+ userId = editor.getValue();
+ } catch (UserIdEditor.NoNameException e) {
+ throw new PgpMain.PgpGeneralException(
+ this.getString(R.string.error_userIdNeedsAName));
+ } catch (UserIdEditor.NoEmailException e) {
+ throw new PgpMain.PgpGeneralException(
+ this.getString(R.string.error_userIdNeedsAnEmailAddress));
+ } catch (UserIdEditor.InvalidEmailException e) {
+ throw new PgpMain.PgpGeneralException(e.getMessage());
+ }
+
+ if (userId.equals("")) {
+ continue;
+ }
+
+ if (editor.isMainUserId()) {
+ userIds.add(0, userId);
+ gotMainUserId = true;
+ } else {
+ userIds.add(userId);
+ }
+ }
+
+ if (userIds.size() == 0) {
+ throw new PgpMain.PgpGeneralException(getString(R.string.error_keyNeedsAUserId));
+ }
+
+ if (!gotMainUserId) {
+ throw new PgpMain.PgpGeneralException(
+ getString(R.string.error_mainUserIdMustNotBeEmpty));
+ }
+
+ return userIds;
+ }
+
+ /**
+ * Returns keys from the SectionView
+ *
+ * @param keysView
+ * @return
+ */
+ private ArrayList<PGPSecretKey> getKeys(SectionView keysView)
+ throws PgpMain.PgpGeneralException {
+ ArrayList<PGPSecretKey> keys = new ArrayList<PGPSecretKey>();
+
+ ViewGroup keyEditors = keysView.getEditors();
+
+ if (keyEditors.getChildCount() == 0) {
+ throw new PgpMain.PgpGeneralException(getString(R.string.error_keyNeedsMasterKey));
+ }
+
+ for (int i = 0; i < keyEditors.getChildCount(); ++i) {
+ KeyEditor editor = (KeyEditor) keyEditors.getChildAt(i);
+ keys.add(editor.getValue());
+ }
+
+ return keys;
+ }
+
+ /**
+ * Returns usage selections of keys from the SectionView
+ *
+ * @param keysView
+ * @return
+ */
+ private ArrayList<Integer> getKeysUsages(SectionView keysView)
+ throws PgpMain.PgpGeneralException {
+ ArrayList<Integer> getKeysUsages = new ArrayList<Integer>();
+
+ ViewGroup keyEditors = keysView.getEditors();
+
+ if (keyEditors.getChildCount() == 0) {
+ throw new PgpMain.PgpGeneralException(getString(R.string.error_keyNeedsMasterKey));
+ }
+
+ for (int i = 0; i < keyEditors.getChildCount(); ++i) {
+ KeyEditor editor = (KeyEditor) keyEditors.getChildAt(i);
+ getKeysUsages.add(editor.getUsage());
+ }
+
+ return getKeysUsages;
+ }
+
+ private void updatePassPhraseButtonText() {
+ mChangePassPhrase.setText(isPassphraseSet() ? R.string.btn_changePassPhrase
+ : R.string.btn_setPassPhrase);
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EncryptActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EncryptActivity.java
new file mode 100644
index 000000000..bb61fb890
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EncryptActivity.java
@@ -0,0 +1,1105 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * 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.sufficientlysecure.keychain.ui;
+
+import org.spongycastle.openpgp.PGPPublicKey;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.compatibility.ClipboardReflection;
+import org.sufficientlysecure.keychain.helper.FileHelper;
+import org.sufficientlysecure.keychain.helper.OtherHelper;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.helper.PgpMain;
+import org.sufficientlysecure.keychain.helper.Preferences;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.service.PassphraseCacheService;
+import org.sufficientlysecure.keychain.ui.dialog.DeleteFileDialogFragment;
+import org.sufficientlysecure.keychain.ui.dialog.FileDialogFragment;
+import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment;
+import org.sufficientlysecure.keychain.util.Choice;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.app.ProgressDialog;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Messenger;
+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.File;
+import java.util.Vector;
+
+public class EncryptActivity extends SherlockFragmentActivity {
+
+ /* Intents */
+ // without permission
+ public static final String ACTION_ENCRYPT = Constants.INTENT_PREFIX + "ENCRYPT";
+ public static final String ACTION_ENCRYPT_FILE = Constants.INTENT_PREFIX + "ENCRYPT_FILE";
+
+ // with permission
+ public static final String ACTION_ENCRYPT_AND_RETURN = Constants.INTENT_PREFIX
+ + "ENCRYPT_AND_RETURN";
+ public static final String ACTION_GENERATE_SIGNATURE_AND_RETURN = Constants.INTENT_PREFIX
+ + "GENERATE_SIGNATURE_AND_RETURN";
+ public static final String ACTION_ENCRYPT_STREAM_AND_RETURN = Constants.INTENT_PREFIX
+ + "ENCRYPT_STREAM_AND_RETURN";
+
+ /* EXTRA keys for input */
+ public static final String EXTRA_TEXT = "text";
+ public static final String EXTRA_DATA = "data";
+ public static final String EXTRA_ASCII_ARMOUR = "asciiArmour";
+ public static final String EXTRA_SEND_TO = "sendTo";
+ public static final String EXTRA_SUBJECT = "subject";
+ public static final String EXTRA_SIGNATURE_KEY_ID = "signatureKeyId";
+ public static final String EXTRA_ENCRYPTION_KEY_IDS = "encryptionKeyIds";
+
+ private String mSubject = null;
+ private String mSendTo = null;
+
+ private long mEncryptionKeyIds[] = null;
+
+ private boolean mEncryptImmediately = false;
+ private EditText mMessage = null;
+ private Button mSelectKeysButton = null;
+
+ private boolean mEncryptEnabled = false;
+ private String mEncryptString = "";
+ private boolean mEncryptToClipboardEnabled = false;
+ private String mEncryptToClipboardString = "";
+
+ 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 mAsciiArmorDemand = false;
+ private boolean mOverrideAsciiArmour = false;
+ private Uri mStreamAndReturnUri = null;
+ private byte[] mData = null;
+
+ private boolean mGenerateSignature = false;
+
+ private long mSecretKeyId = Id.key.none;
+
+ private FileDialogFragment mFileDialog;
+
+ /**
+ * ActionBar menu is created based on class variables to change it at runtime
+ *
+ */
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ if (mEncryptToClipboardEnabled) {
+ menu.add(1, Id.menu.option.encrypt_to_clipboard, 0, mEncryptToClipboardString)
+ .setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ }
+ if (mEncryptEnabled) {
+ menu.add(1, Id.menu.option.encrypt, 1, mEncryptString).setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go home
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+
+ case Id.menu.option.encrypt_to_clipboard:
+ encryptToClipboardClicked();
+
+ return true;
+
+ case Id.menu.option.encrypt:
+ encryptClicked();
+
+ return true;
+
+ default:
+ return super.onOptionsItemSelected(item);
+
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // check permissions for intent actions without user interaction
+ String[] restrictedActions = new String[] { ACTION_ENCRYPT_AND_RETURN,
+ ACTION_GENERATE_SIGNATURE_AND_RETURN, ACTION_ENCRYPT_STREAM_AND_RETURN };
+ OtherHelper.checkPackagePermissionForActions(this, this.getCallingPackage(),
+ Constants.PERMISSION_ACCESS_API, getIntent().getAction(), restrictedActions);
+
+ setContentView(R.layout.encrypt);
+
+ // set actionbar without home button if called from another app
+ OtherHelper.setActionBarBackButton(this);
+
+ initView();
+
+ // Handle intent actions
+ handleActions(getIntent());
+
+ updateView();
+ updateSource();
+ updateMode();
+
+ if (mEncryptImmediately) {
+ 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);
+ }
+
+ updateActionBarButtons();
+
+ if (mEncryptImmediately
+ && (mMessage.getText().length() > 0 || mData != null)
+ && ((mEncryptionKeyIds != null && mEncryptionKeyIds.length > 0) || mSecretKeyId != 0)) {
+ encryptClicked();
+ }
+ }
+
+ /**
+ * Handles all actions with this intent
+ *
+ * @param intent
+ */
+ private void handleActions(Intent intent) {
+ String action = intent.getAction();
+ Bundle extras = intent.getExtras();
+ String type = intent.getType();
+ Uri uri = intent.getData();
+
+ if (extras == null) {
+ extras = new Bundle();
+ }
+
+ /*
+ * Android's Action
+ */
+ if (Intent.ACTION_SEND.equals(action) && type != null) {
+ // When sending to APG Encrypt via share menu
+ if ("text/plain".equals(type)) {
+ // Plain text
+ String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
+ if (sharedText != null) {
+ // handle like normal text encryption, override action and extras to later
+ // execute ACTION_ENCRYPT in main actions
+ extras.putString(EXTRA_TEXT, sharedText);
+ extras.putBoolean(EXTRA_ASCII_ARMOUR, true);
+ action = ACTION_ENCRYPT;
+ }
+ } else {
+ // Files via content provider, override uri and action
+ uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
+ action = ACTION_ENCRYPT_FILE;
+ }
+ }
+
+ if (ACTION_ENCRYPT_AND_RETURN.equals(action)
+ || ACTION_GENERATE_SIGNATURE_AND_RETURN.equals(action)) {
+ mEncryptImmediately = true;
+ }
+
+ if (ACTION_GENERATE_SIGNATURE_AND_RETURN.equals(action)) {
+ mGenerateSignature = true;
+ mOverrideAsciiArmour = true;
+ mAsciiArmorDemand = false;
+ }
+
+ if (extras.containsKey(EXTRA_ASCII_ARMOUR)) {
+ mAsciiArmorDemand = extras.getBoolean(EXTRA_ASCII_ARMOUR, true);
+ mOverrideAsciiArmour = true;
+ mAsciiArmour.setChecked(mAsciiArmorDemand);
+ }
+
+ mData = extras.getByteArray(EXTRA_DATA);
+ String textData = null;
+ if (mData == null) {
+ textData = extras.getString(EXTRA_TEXT);
+ }
+ mSendTo = extras.getString(EXTRA_SEND_TO);
+ mSubject = extras.getString(EXTRA_SUBJECT);
+ long signatureKeyId = extras.getLong(EXTRA_SIGNATURE_KEY_ID);
+ long[] encryptionKeyIds = extras.getLongArray(EXTRA_ENCRYPTION_KEY_IDS);
+
+ // preselect keys given by intent
+ preselectKeys(signatureKeyId, encryptionKeyIds);
+
+ /**
+ * Main Actions
+ */
+ if (ACTION_ENCRYPT.equals(action) || ACTION_ENCRYPT_AND_RETURN.equals(action)
+ || ACTION_GENERATE_SIGNATURE_AND_RETURN.equals(action)) {
+ if (textData != null) {
+ mMessage.setText(textData);
+ }
+ mSource.setInAnimation(null);
+ mSource.setOutAnimation(null);
+ while (mSource.getCurrentView().getId() != R.id.sourceMessage) {
+ mSource.showNext();
+ }
+ } else if (ACTION_ENCRYPT_FILE.equals(action)) {
+ // get file path from uri
+ String path = FileHelper.getPath(this, uri);
+
+ if (path != null) {
+ mInputFilename = path;
+ mFilename.setText(mInputFilename);
+
+ mSource.setInAnimation(null);
+ mSource.setOutAnimation(null);
+ while (mSource.getCurrentView().getId() != R.id.sourceFile) {
+ mSource.showNext();
+ }
+ } else {
+ Log.e(Constants.TAG,
+ "Direct binary data without actual file in filesystem is not supported. This is only supported by ACTION_ENCRYPT_STREAM_AND_RETURN.");
+ Toast.makeText(this, R.string.error_onlyFilesAreSupported, Toast.LENGTH_LONG)
+ .show();
+ // end activity
+ finish();
+ }
+ } else if (ACTION_ENCRYPT_STREAM_AND_RETURN.equals(action)) {
+ // TODO: Set mStreamAndReturnUri that is used later to encrypt a stream!
+
+ mStreamAndReturnUri = uri;
+ }
+ }
+
+ /**
+ * If an Intent gives a signatureKeyId and/or encryptionKeyIds, preselect those!
+ *
+ * @param preselectedSignatureKeyId
+ * @param preselectedEncryptionKeyIds
+ */
+ private void preselectKeys(long preselectedSignatureKeyId, long[] preselectedEncryptionKeyIds) {
+ if (preselectedSignatureKeyId != 0) {
+ PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingByMasterKeyId(this,
+ preselectedSignatureKeyId);
+ PGPSecretKey masterKey = null;
+ if (keyRing != null) {
+ masterKey = PgpHelper.getMasterKey(keyRing);
+ if (masterKey != null) {
+ Vector<PGPSecretKey> signKeys = PgpHelper.getUsableSigningKeys(keyRing);
+ if (signKeys.size() > 0) {
+ mSecretKeyId = masterKey.getKeyID();
+ }
+ }
+ }
+ }
+
+ if (preselectedEncryptionKeyIds != null) {
+ Vector<Long> goodIds = new Vector<Long>();
+ for (int i = 0; i < preselectedEncryptionKeyIds.length; ++i) {
+ PGPPublicKeyRing keyRing = ProviderHelper.getPGPPublicKeyRingByMasterKeyId(this,
+ preselectedEncryptionKeyIds[i]);
+ PGPPublicKey masterKey = null;
+ if (keyRing == null) {
+ continue;
+ }
+ masterKey = PgpHelper.getMasterKey(keyRing);
+ if (masterKey == null) {
+ continue;
+ }
+ Vector<PGPPublicKey> encryptKeys = PgpHelper.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);
+ }
+ }
+ }
+ }
+
+ /**
+ * Guess output filename based on input path
+ *
+ * @param path
+ * @return Suggestion for output filename
+ */
+ private String guessOutputFilename(String path) {
+ // output in the same directory but with additional ending
+ File file = new File(path);
+ String ending = (mAsciiArmour.isChecked() ? ".asc" : ".gpg");
+ String outputFilename = file.getParent() + File.separator + file.getName() + ending;
+
+ return outputFilename;
+ }
+
+ 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;
+ }
+ }
+ updateActionBarButtons();
+ }
+
+ /**
+ * Set ActionBar buttons based on parameters
+ *
+ * @param encryptEnabled
+ * @param encryptStringRes
+ * @param encryptToClipboardEnabled
+ * @param encryptToClipboardStringRes
+ */
+ private void setActionbarButtons(boolean encryptEnabled, int encryptStringRes,
+ boolean encryptToClipboardEnabled, int encryptToClipboardStringRes) {
+ mEncryptEnabled = encryptEnabled;
+ if (encryptEnabled) {
+ mEncryptString = getString(encryptStringRes);
+ }
+ mEncryptToClipboardEnabled = encryptToClipboardEnabled;
+ if (encryptToClipboardEnabled) {
+ mEncryptToClipboardString = getString(encryptToClipboardStringRes);
+ }
+
+ // build new action bar based on these class variables
+ invalidateOptionsMenu();
+ }
+
+ /**
+ * Update ActionBar buttons based on current selection in view
+ */
+ private void updateActionBarButtons() {
+ switch (mSource.getCurrentView().getId()) {
+ case R.id.sourceFile: {
+ setActionbarButtons(true, R.string.btn_encryptFile, false, 0);
+
+ break;
+ }
+
+ case R.id.sourceMessage: {
+ mSourceLabel.setText(R.string.label_message);
+
+ if (mMode.getCurrentView().getId() == R.id.modeSymmetric) {
+ if (mEncryptImmediately) {
+ setActionbarButtons(true, R.string.btn_encrypt, false, 0);
+ } else {
+ setActionbarButtons(true, R.string.btn_encryptAndEmail, true,
+ R.string.btn_encryptToClipboard);
+ }
+ } else {
+ if (mEncryptionKeyIds == null || mEncryptionKeyIds.length == 0) {
+ if (mSecretKeyId == 0) {
+ setActionbarButtons(false, 0, false, 0);
+ } else {
+ if (mEncryptImmediately) {
+ setActionbarButtons(true, R.string.btn_sign, false, 0);
+ } else {
+ setActionbarButtons(true, R.string.btn_signAndEmail, true,
+ R.string.btn_signToClipboard);
+ }
+ }
+ } else {
+ if (mEncryptImmediately) {
+ setActionbarButtons(true, R.string.btn_encrypt, false, 0);
+ } else {
+ setActionbarButtons(true, R.string.btn_encryptAndEmail, true,
+ R.string.btn_encryptToClipboard);
+ }
+ }
+ }
+ 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;
+ }
+ }
+ updateActionBarButtons();
+ }
+
+ private void encryptToClipboardClicked() {
+ mEncryptTarget = Id.target.clipboard;
+ initiateEncryption();
+ }
+
+ private void encryptClicked() {
+ Log.d(Constants.TAG, "encryptClicked invoked!");
+
+ 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)) {
+ mInputFilename = mFilename.getText().toString();
+ }
+
+ mOutputFilename = guessOutputFilename(mInputFilename);
+
+ 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 && mSecretKeyId == 0) {
+ Toast.makeText(this, R.string.selectEncryptionOrSignatureKey, Toast.LENGTH_SHORT)
+ .show();
+ return;
+ }
+
+ if (mSecretKeyId != 0
+ && PassphraseCacheService.getCachedPassphrase(this, mSecretKeyId) == null) {
+ showPassphraseDialog();
+
+ return;
+ }
+ }
+
+ if (mEncryptTarget == Id.target.file) {
+ showOutputFileDialog();
+ } else {
+ encryptStart();
+ }
+ }
+
+ /**
+ * Shows passphrase dialog to cache a new passphrase the user enters for using it later for
+ * encryption
+ */
+ private void showPassphraseDialog() {
+ // Message is received after passphrase is cached
+ Handler returnHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) {
+ if (mEncryptTarget == Id.target.file) {
+ showOutputFileDialog();
+ } else {
+ encryptStart();
+ }
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(returnHandler);
+
+ try {
+ PassphraseDialogFragment passphraseDialog = PassphraseDialogFragment.newInstance(
+ EncryptActivity.this, messenger, mSecretKeyId);
+
+ passphraseDialog.show(getSupportFragmentManager(), "passphraseDialog");
+ } catch (PgpMain.PgpGeneralException e) {
+ Log.d(Constants.TAG, "No passphrase for this secret key, encrypt directly!");
+ // send message to handler to start encryption directly
+ returnHandler.sendEmptyMessage(PassphraseDialogFragment.MESSAGE_OKAY);
+ }
+ }
+
+ private void showOutputFileDialog() {
+ // Message is received after file is selected
+ Handler returnHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == FileDialogFragment.MESSAGE_OKAY) {
+ Bundle data = message.getData();
+ mOutputFilename = data.getString(FileDialogFragment.MESSAGE_DATA_FILENAME);
+ encryptStart();
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(returnHandler);
+
+ mFileDialog = FileDialogFragment.newInstance(messenger,
+ getString(R.string.title_encryptToFile),
+ getString(R.string.specifyFileToEncryptTo), mOutputFilename, null,
+ Id.request.output_filename);
+
+ mFileDialog.show(getSupportFragmentManager(), "fileDialog");
+ }
+
+ private void encryptStart() {
+ // Send all information needed to service to edit key in other thread
+ Intent intent = new Intent(this, KeychainIntentService.class);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ boolean useAsciiArmor = true;
+ long encryptionKeyIds[] = null;
+ int compressionId = 0;
+ boolean signOnly = false;
+
+ if (mMode.getCurrentView().getId() == R.id.modeSymmetric) {
+ Log.d(Constants.TAG, "Symmetric encryption enabled!");
+ String passPhrase = mPassPhrase.getText().toString();
+ if (passPhrase.length() == 0) {
+ passPhrase = null;
+ }
+
+ data.putString(KeychainIntentService.GENERATE_KEY_SYMMETRIC_PASSPHRASE, passPhrase);
+ } else {
+ encryptionKeyIds = mEncryptionKeyIds;
+ signOnly = (mEncryptionKeyIds == null || mEncryptionKeyIds.length == 0);
+ }
+
+ intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_ENCRYPT_SIGN);
+
+ // choose default settings, target and data bundle by target
+ if (mStreamAndReturnUri != null) {
+ // mIntentDataUri is only defined when ACTION_ENCRYPT_STREAM_AND_RETURN is used
+ data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_STREAM);
+ data.putParcelable(KeychainIntentService.ENCRYPT_PROVIDER_URI, mStreamAndReturnUri);
+
+ } else if (mEncryptTarget == Id.target.file) {
+ useAsciiArmor = mAsciiArmour.isChecked();
+ compressionId = ((Choice) mFileCompression.getSelectedItem()).getId();
+
+ data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_FILE);
+
+ Log.d(Constants.TAG, "mInputFilename=" + mInputFilename + ", mOutputFilename="
+ + mOutputFilename);
+
+ data.putString(KeychainIntentService.ENCRYPT_INPUT_FILE, mInputFilename);
+ data.putString(KeychainIntentService.ENCRYPT_OUTPUT_FILE, mOutputFilename);
+
+ } else {
+ useAsciiArmor = true;
+ compressionId = Preferences.getPreferences(this).getDefaultMessageCompression();
+
+ data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_BYTES);
+
+ if (mData != null) {
+ data.putByteArray(KeychainIntentService.ENCRYPT_MESSAGE_BYTES, mData);
+ } else {
+ String message = mMessage.getText().toString();
+ if (signOnly && !mEncryptImmediately) {
+ fixBadCharactersForGmail(message);
+ }
+ data.putByteArray(KeychainIntentService.ENCRYPT_MESSAGE_BYTES, message.getBytes());
+ }
+ }
+
+ if (mOverrideAsciiArmour) {
+ useAsciiArmor = mAsciiArmorDemand;
+ }
+
+ data.putLong(KeychainIntentService.ENCRYPT_SECRET_KEY_ID, mSecretKeyId);
+ data.putBoolean(KeychainIntentService.ENCRYPT_USE_ASCII_AMOR, useAsciiArmor);
+ data.putLongArray(KeychainIntentService.ENCRYPT_ENCRYPTION_KEYS_IDS, encryptionKeyIds);
+ data.putInt(KeychainIntentService.ENCRYPT_COMPRESSION_ID, compressionId);
+ data.putBoolean(KeychainIntentService.ENCRYPT_GENERATE_SIGNATURE, mGenerateSignature);
+ data.putBoolean(KeychainIntentService.ENCRYPT_SIGN_ONLY, signOnly);
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after encrypting is done in ApgService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
+ R.string.progress_encrypting, ProgressDialog.STYLE_HORIZONTAL) {
+ public void handleMessage(Message message) {
+ // handle messages by standard ApgHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ // get returned data bundle
+ Bundle data = message.getData();
+
+ String output;
+ switch (mEncryptTarget) {
+ case Id.target.clipboard:
+ output = data.getString(KeychainIntentService.RESULT_ENCRYPTED_STRING);
+ Log.d(Constants.TAG, "output: " + output);
+ ClipboardReflection.copyToClipboard(EncryptActivity.this, output);
+ Toast.makeText(EncryptActivity.this,
+ R.string.encryptionToClipboardSuccessful, Toast.LENGTH_SHORT)
+ .show();
+ break;
+
+ case Id.target.email:
+ if (mEncryptImmediately) {
+ Intent intent = new Intent();
+ intent.putExtras(data);
+ setResult(RESULT_OK, intent);
+ finish();
+ return;
+ }
+
+ output = data.getString(KeychainIntentService.RESULT_ENCRYPTED_STRING);
+ Log.d(Constants.TAG, "output: " + output);
+
+ Intent emailIntent = new Intent(Intent.ACTION_SEND);
+ emailIntent.setType("text/plain; charset=utf-8");
+ emailIntent.putExtra(Intent.EXTRA_TEXT, output);
+ if (mSubject != null) {
+ emailIntent.putExtra(Intent.EXTRA_SUBJECT, mSubject);
+ }
+ if (mSendTo != null) {
+ emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[] { mSendTo });
+ }
+ startActivity(Intent.createChooser(emailIntent,
+ getString(R.string.title_sendEmail)));
+ break;
+
+ case Id.target.file:
+ Toast.makeText(EncryptActivity.this, R.string.encryptionSuccessful,
+ Toast.LENGTH_SHORT).show();
+
+ if (mDeleteAfter.isChecked()) {
+ // Create and show dialog to delete original file
+ DeleteFileDialogFragment deleteFileDialog = DeleteFileDialogFragment
+ .newInstance(mInputFilename);
+ deleteFileDialog.show(getSupportFragmentManager(), "deleteDialog");
+ }
+ break;
+
+ default:
+ // shouldn't happen
+ break;
+
+ }
+ }
+ };
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // show progress dialog
+ saveHandler.showProgressDialog(this);
+
+ // start service with intent
+ startService(intent);
+ }
+
+ /**
+ * Fixes bad message characters for gmail
+ *
+ * @param message
+ * @return
+ */
+ private String fixBadCharactersForGmail(String message) {
+ // 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");
+
+ return message;
+ }
+
+ private void initView() {
+ 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);
+ 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) {
+ FileHelper.openFile(EncryptActivity.this, mFilename.getText().toString(), "*/*",
+ Id.request.filename);
+ }
+ });
+
+ 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 = Preferences.getPreferences(this).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(Preferences.getPreferences(this).getDefaultAsciiArmour());
+
+ 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 {
+ mSecretKeyId = Id.key.none;
+ updateView();
+ }
+ }
+ });
+ }
+
+ 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 (mSecretKeyId == Id.key.none) {
+ mSign.setChecked(false);
+ mMainUserId.setText("");
+ mMainUserIdRest.setText("");
+ } else {
+ String uid = getResources().getString(R.string.unknownUserId);
+ String uidExtra = "";
+ PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingByMasterKeyId(this,
+ mSecretKeyId);
+ if (keyRing != null) {
+ PGPSecretKey key = PgpHelper.getMasterKey(keyRing);
+ if (key != null) {
+ String userId = PgpHelper.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);
+ }
+
+ updateActionBarButtons();
+ }
+
+ private void selectPublicKeys() {
+ Intent intent = new Intent(this, SelectPublicKeyActivity.class);
+ Vector<Long> keyIds = new Vector<Long>();
+ if (mSecretKeyId != 0) {
+ keyIds.add(mSecretKeyId);
+ }
+ 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(SelectPublicKeyActivity.EXTRA_SELECTED_MASTER_KEY_IDS, initialKeyIds);
+ startActivityForResult(intent, Id.request.public_keys);
+ }
+
+ private void selectSecretKey() {
+ Intent intent = new Intent(this, SelectSecretKeyActivity.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) {
+ try {
+ String path = FileHelper.getPath(this, data.getData());
+ Log.d(Constants.TAG, "path=" + path);
+
+ mFilename.setText(path);
+ } catch (NullPointerException e) {
+ Log.e(Constants.TAG, "Nullpointer while retrieving path!");
+ }
+ }
+ return;
+ }
+
+ case Id.request.output_filename: {
+ if (resultCode == RESULT_OK && data != null) {
+ try {
+ String path = data.getData().getPath();
+ Log.d(Constants.TAG, "path=" + path);
+
+ mFileDialog.setFilename(path);
+ } catch (NullPointerException e) {
+ Log.e(Constants.TAG, "Nullpointer while retrieving path!");
+ }
+ }
+ return;
+ }
+
+ case Id.request.public_keys: {
+ if (resultCode == RESULT_OK) {
+ Bundle bundle = data.getExtras();
+ mEncryptionKeyIds = bundle
+ .getLongArray(SelectPublicKeyActivity.RESULT_EXTRA_MASTER_KEY_IDS);
+ }
+ updateView();
+ break;
+ }
+
+ case Id.request.secret_keys: {
+ if (resultCode == RESULT_OK) {
+ Bundle bundle = data.getExtras();
+ mSecretKeyId = bundle.getLong(SelectSecretKeyActivity.RESULT_EXTRA_MASTER_KEY_ID);
+ } else {
+ mSecretKeyId = Id.key.none;
+ }
+ updateView();
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpActivity.java
new file mode 100644
index 000000000..5b2750742
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpActivity.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * 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.sufficientlysecure.keychain.ui;
+
+import org.sufficientlysecure.keychain.R;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.FragmentTransaction;
+import android.widget.TextView;
+
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.app.ActionBar.Tab;
+import com.actionbarsherlock.view.MenuItem;
+
+import java.util.ArrayList;
+
+import android.content.Context;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentPagerAdapter;
+import android.support.v4.view.ViewPager;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+
+public class HelpActivity extends SherlockFragmentActivity {
+ public static final String EXTRA_SELECTED_TAB = "selectedTab";
+
+ ViewPager mViewPager;
+ TabsAdapter mTabsAdapter;
+ TextView tabCenter;
+ TextView tabText;
+
+ /**
+ * Menu Items
+ */
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go home
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mViewPager = new ViewPager(this);
+ mViewPager.setId(R.id.pager);
+
+ setContentView(mViewPager);
+ ActionBar bar = getSupportActionBar();
+ bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+ bar.setDisplayShowTitleEnabled(true);
+ bar.setDisplayHomeAsUpEnabled(true);
+
+ mTabsAdapter = new TabsAdapter(this, mViewPager);
+
+ int selectedTab = 0;
+ Intent intent = getIntent();
+ if (intent.getExtras() != null && intent.getExtras().containsKey(EXTRA_SELECTED_TAB)) {
+ selectedTab = intent.getExtras().getInt(EXTRA_SELECTED_TAB);
+ }
+
+ Bundle startBundle = new Bundle();
+ startBundle.putInt(HelpFragmentHtml.ARG_HTML_FILE, R.raw.help_start);
+ mTabsAdapter.addTab(bar.newTab().setText(getString(R.string.help_tab_start)),
+ HelpFragmentHtml.class, startBundle, (selectedTab == 0 ? true : false));
+
+ Bundle nfcBundle = new Bundle();
+ nfcBundle.putInt(HelpFragmentHtml.ARG_HTML_FILE, R.raw.help_nfc_beam);
+ mTabsAdapter.addTab(bar.newTab().setText(getString(R.string.help_tab_nfc_beam)),
+ HelpFragmentHtml.class, nfcBundle, (selectedTab == 1 ? true : false));
+
+ Bundle changelogBundle = new Bundle();
+ changelogBundle.putInt(HelpFragmentHtml.ARG_HTML_FILE, R.raw.help_changelog);
+ mTabsAdapter.addTab(bar.newTab().setText(getString(R.string.help_tab_changelog)),
+ HelpFragmentHtml.class, changelogBundle, (selectedTab == 2 ? true : false));
+
+ mTabsAdapter.addTab(bar.newTab().setText(getString(R.string.help_tab_about)),
+ HelpFragmentAbout.class, null, (selectedTab == 3 ? true : false));
+ }
+
+ public static class TabsAdapter extends FragmentPagerAdapter implements ActionBar.TabListener,
+ ViewPager.OnPageChangeListener {
+ private final Context mContext;
+ private final ActionBar mActionBar;
+ private final ViewPager mViewPager;
+ private final ArrayList<TabInfo> mTabs = new ArrayList<TabInfo>();
+
+ static final class TabInfo {
+ private final Class<?> clss;
+ private final Bundle args;
+
+ TabInfo(Class<?> _class, Bundle _args) {
+ clss = _class;
+ args = _args;
+ }
+ }
+
+ public TabsAdapter(SherlockFragmentActivity activity, ViewPager pager) {
+ super(activity.getSupportFragmentManager());
+ mContext = activity;
+ mActionBar = activity.getSupportActionBar();
+ mViewPager = pager;
+ mViewPager.setAdapter(this);
+ mViewPager.setOnPageChangeListener(this);
+ }
+
+ public void addTab(ActionBar.Tab tab, Class<?> clss, Bundle args, boolean selected) {
+ TabInfo info = new TabInfo(clss, args);
+ tab.setTag(info);
+ tab.setTabListener(this);
+ mTabs.add(info);
+ mActionBar.addTab(tab, selected);
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getCount() {
+ return mTabs.size();
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ TabInfo info = mTabs.get(position);
+ return Fragment.instantiate(mContext, info.clss.getName(), info.args);
+ }
+
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ }
+
+ public void onPageSelected(int position) {
+ mActionBar.setSelectedNavigationItem(position);
+ }
+
+ public void onPageScrollStateChanged(int state) {
+ }
+
+ public void onTabSelected(Tab tab, FragmentTransaction ft) {
+ Object tag = tab.getTag();
+ for (int i = 0; i < mTabs.size(); i++) {
+ if (mTabs.get(i) == tag) {
+ mViewPager.setCurrentItem(i);
+ }
+ }
+ }
+
+ public void onTabUnselected(Tab tab, FragmentTransaction ft) {
+ }
+
+ public void onTabReselected(Tab tab, FragmentTransaction ft) {
+ }
+ }
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentAbout.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentAbout.java
new file mode 100644
index 000000000..05891b053
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentAbout.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * 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.sufficientlysecure.keychain.ui;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import net.nightwhistler.htmlspanner.HtmlSpanner;
+import net.nightwhistler.htmlspanner.JellyBeanSpanFixTextView;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.helper.OtherHelper;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.text.method.LinkMovementMethod;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.actionbarsherlock.app.SherlockFragment;
+
+public class HelpFragmentAbout extends SherlockFragment {
+
+ /**
+ * Workaround for Android Bug. See
+ * http://stackoverflow.com/questions/8748064/starting-activity-from
+ * -fragment-causes-nullpointerexception
+ */
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ setUserVisibleHint(true);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.help_fragment_about, container, false);
+
+ // load html from html file from /res/raw
+ InputStream inputStreamText = getResources().openRawResource(R.raw.help_about);
+
+ TextView versionText = (TextView) view.findViewById(R.id.help_about_version);
+ versionText.setText(getString(R.string.help_about_version) + " " + getVersion());
+
+ JellyBeanSpanFixTextView aboutTextView = (JellyBeanSpanFixTextView) view
+ .findViewById(R.id.help_about_text);
+
+ // load html into textview
+ HtmlSpanner htmlSpanner = new HtmlSpanner();
+ htmlSpanner.setStripExtraWhiteSpace(true);
+ try {
+ aboutTextView.setText(htmlSpanner.fromHtml(inputStreamText));
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "Error while reading raw resources as stream", e);
+ }
+
+ // make links work
+ aboutTextView.setMovementMethod(LinkMovementMethod.getInstance());
+
+ // no flickering when clicking textview for Android < 4
+ aboutTextView.setTextColor(getResources().getColor(android.R.color.black));
+
+ return view;
+ }
+
+ /**
+ * Get the current package version.
+ *
+ * @return The current version.
+ */
+ private String getVersion() {
+ String result = "";
+ try {
+ PackageManager manager = getActivity().getPackageManager();
+ PackageInfo info = manager.getPackageInfo(getActivity().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;
+ }
+
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentHtml.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentHtml.java
new file mode 100644
index 000000000..9c0bb6ded
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentHtml.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * 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.sufficientlysecure.keychain.ui;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import net.nightwhistler.htmlspanner.HtmlSpanner;
+import net.nightwhistler.htmlspanner.JellyBeanSpanFixTextView;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.helper.OtherHelper;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.text.method.LinkMovementMethod;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ScrollView;
+
+import com.actionbarsherlock.app.SherlockFragment;
+
+public class HelpFragmentHtml extends SherlockFragment {
+ private Activity mActivity;
+
+ private int htmlFile;
+
+ public static final String ARG_HTML_FILE = "htmlFile";
+
+ /**
+ * Create a new instance of HelpFragmentHtml, providing "htmlFile" as an argument.
+ */
+ static HelpFragmentHtml newInstance(int htmlFile) {
+ HelpFragmentHtml f = new HelpFragmentHtml();
+
+ // Supply html raw file input as an argument.
+ Bundle args = new Bundle();
+ args.putInt(ARG_HTML_FILE, htmlFile);
+ f.setArguments(args);
+
+ return f;
+ }
+
+ /**
+ * Workaround for Android Bug. See
+ * http://stackoverflow.com/questions/8748064/starting-activity-from
+ * -fragment-causes-nullpointerexception
+ */
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ setUserVisibleHint(true);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ htmlFile = getArguments().getInt(ARG_HTML_FILE);
+
+ // load html from html file from /res/raw
+ InputStream inputStreamText = getResources().openRawResource(htmlFile);
+
+ mActivity = getActivity();
+
+ ScrollView scroller = new ScrollView(mActivity);
+ JellyBeanSpanFixTextView text = new JellyBeanSpanFixTextView(mActivity);
+
+ // padding
+ int padding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, mActivity
+ .getResources().getDisplayMetrics());
+ text.setPadding(padding, padding, padding, 0);
+
+ scroller.addView(text);
+
+ // load html into textview
+ HtmlSpanner htmlSpanner = new HtmlSpanner();
+ htmlSpanner.setStripExtraWhiteSpace(true);
+ try {
+ text.setText(htmlSpanner.fromHtml(inputStreamText));
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "Error while reading raw resources as stream", e);
+ }
+
+ // make links work
+ text.setMovementMethod(LinkMovementMethod.getInstance());
+
+ // no flickering when clicking textview for Android < 4
+ text.setTextColor(getResources().getColor(android.R.color.black));
+
+ return scroller;
+ }
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java
new file mode 100644
index 000000000..cd5a12df0
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java
@@ -0,0 +1,473 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2011 Senecaso
+ *
+ * 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.sufficientlysecure.keychain.ui;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
+import org.sufficientlysecure.keychain.helper.OtherHelper;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.ui.dialog.DeleteFileDialogFragment;
+import org.sufficientlysecure.keychain.ui.dialog.FileDialogFragment;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Messenger;
+
+
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentTransaction;
+import android.view.View;
+import android.widget.Toast;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+import com.google.zxing.integration.android.IntentIntegrator;
+import com.google.zxing.integration.android.IntentResult;
+
+public class ImportKeysActivity extends SherlockFragmentActivity {
+ public static final String ACTION_IMPORT = Constants.INTENT_PREFIX + "IMPORT";
+ public static final String ACTION_IMPORT_FROM_FILE = Constants.INTENT_PREFIX
+ + "IMPORT_FROM_FILE";
+ public static final String ACTION_IMPORT_FROM_QR_CODE = Constants.INTENT_PREFIX
+ + "IMPORT_FROM_QR_CODE";
+ public static final String ACTION_IMPORT_FROM_NFC = Constants.INTENT_PREFIX + "IMPORT_FROM_NFC";
+
+ // only used by IMPORT
+ public static final String EXTRA_TEXT = "text";
+ public static final String EXTRA_KEYRING_BYTES = "keyringBytes";
+
+ // public static final String EXTRA_KEY_ID = "keyId";
+
+ protected String mImportFilename;
+ protected byte[] mImportData;
+
+ protected boolean mDeleteAfterImport = false;
+
+ FileDialogFragment mFileDialog;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.import_keys);
+
+ // set actionbar without home button if called from another app
+ OtherHelper.setActionBarBackButton(this);
+
+ handleActions(getIntent());
+ }
+
+ /**
+ * ActionBar menu is created based on class variables to change it at runtime
+ *
+ */
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(1, Id.menu.option.import_from_file, 0, R.string.menu_importFromFile)
+ .setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ menu.add(1, Id.menu.option.import_from_qr_code, 1, R.string.menu_importFromQrCode)
+ .setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ menu.add(1, Id.menu.option.import_from_nfc, 2, R.string.menu_importFromNfc)
+ .setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go home
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+
+ case Id.menu.option.import_from_file:
+ showImportFromFileDialog();
+ return true;
+
+ case Id.menu.option.import_from_qr_code:
+ importFromQrCode();
+ return true;
+
+ case Id.menu.option.import_from_nfc:
+ importFromNfc();
+ return true;
+
+ default:
+ return super.onOptionsItemSelected(item);
+
+ }
+ }
+
+ protected void handleActions(Intent intent) {
+ String action = intent.getAction();
+ Bundle extras = intent.getExtras();
+
+ if (extras == null) {
+ extras = new Bundle();
+ }
+
+ /**
+ * Android Standard Actions
+ */
+ if (Intent.ACTION_VIEW.equals(action)) {
+ // Android's Action when opening file associated to APG (see AndroidManifest.xml)
+ // override action to delegate it to APGs ACTION_IMPORT
+ action = ACTION_IMPORT;
+ }
+
+ /**
+ * APG's own Actions
+ */
+ if (ACTION_IMPORT.equals(action)) {
+ if ("file".equals(intent.getScheme()) && intent.getDataString() != null) {
+ mImportFilename = intent.getData().getPath();
+ mImportData = null;
+ } else if (extras.containsKey(EXTRA_TEXT)) {
+ mImportData = intent.getStringExtra(EXTRA_TEXT).getBytes();
+ mImportFilename = null;
+ } else if (extras.containsKey(EXTRA_KEYRING_BYTES)) {
+ mImportData = intent.getByteArrayExtra(EXTRA_KEYRING_BYTES);
+ mImportFilename = null;
+ }
+ loadKeyListFragment();
+ } else if (ACTION_IMPORT_FROM_FILE.equals(action)) {
+ if ("file".equals(intent.getScheme()) && intent.getDataString() != null) {
+ mImportFilename = intent.getData().getPath();
+ mImportData = null;
+ }
+ showImportFromFileDialog();
+ } else if (ACTION_IMPORT_FROM_QR_CODE.equals(action)) {
+ importFromQrCode();
+ } else if (ACTION_IMPORT_FROM_NFC.equals(action)) {
+ importFromNfc();
+ }
+ }
+
+ public void loadKeyListFragment() {
+ if (mImportData != null || mImportFilename != null) {
+ // generate list of keyrings
+ FragmentManager fragmentManager = getSupportFragmentManager();
+ FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
+
+ ImportKeysListFragment listFragment = new ImportKeysListFragment();
+ Bundle args = new Bundle();
+ args.putByteArray(ImportKeysListFragment.ARG_KEYRING_BYTES, mImportData);
+ args.putString(ImportKeysListFragment.ARG_IMPORT_FILENAME, mImportFilename);
+ listFragment.setArguments(args);
+ // replace container in view with fragment
+ fragmentTransaction.replace(R.id.import_keys_list_container, listFragment);
+ fragmentTransaction.commit();
+ }
+ }
+
+ private void importFromQrCode() {
+ new IntentIntegrator(this).initiateScan();
+ }
+
+ private void importFromNfc() {
+ // show nfc help
+ Intent intent = new Intent(this, HelpActivity.class);
+ intent.putExtra(HelpActivity.EXTRA_SELECTED_TAB, 1);
+ startActivityForResult(intent, 0);
+ }
+
+ /**
+ * Show to dialog from where to import keys
+ */
+ public void showImportFromFileDialog() {
+ // Message is received after file is selected
+ Handler returnHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == FileDialogFragment.MESSAGE_OKAY) {
+ Bundle data = message.getData();
+ mImportFilename = data.getString(FileDialogFragment.MESSAGE_DATA_FILENAME);
+ mDeleteAfterImport = data.getBoolean(FileDialogFragment.MESSAGE_DATA_CHECKED);
+
+ Log.d(Constants.TAG, "mImportFilename: " + mImportFilename);
+ Log.d(Constants.TAG, "mDeleteAfterImport: " + mDeleteAfterImport);
+
+ loadKeyListFragment();
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ final Messenger messenger = new Messenger(returnHandler);
+
+ DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() {
+ public void run() {
+ mFileDialog = FileDialogFragment.newInstance(messenger,
+ getString(R.string.title_importKeys),
+ getString(R.string.specifyFileToImportFrom), Constants.path.APP_DIR + "/",
+ null, Id.request.filename);
+
+ mFileDialog.show(getSupportFragmentManager(), "fileDialog");
+ }
+ });
+ }
+
+ // private void importAndSignOld(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
+ //
+ // // TODO: there should be only 1
+ // HkpKeyServer server = new HkpKeyServer(mPreferences.getKeyServers()[0]);
+ // String encodedKey = server.get(keyId);
+ //
+ // PGPKeyRing keyring = PGPHelper.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 = PGPHelper.convertFingerprintToHex(publicKeyRing
+ // .getPublicKey().getFingerprint());
+ // if (expectedFingerprint.equals(actualFingerprint)) {
+ // // store the signed key in our local cache
+ // int retval = PGPMain.storeKeyRingInCache(publicKeyRing);
+ // if (retval != Id.return_value.ok
+ // && retval != Id.return_value.updated) {
+ // status.putString(EXTRA_ERROR,
+ // "Failed to store signed key in local cache");
+ // } else {
+ // Intent intent = new Intent(ImportFromQRCodeActivity.this,
+ // SignKeyActivity.class);
+ // intent.putExtra(EXTRA_KEY_ID, keyId);
+ // startActivityForResult(intent, Id.request.sign_key);
+ // }
+ // } else {
+ // status.putString(
+ // 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(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(EXTRA_ERROR, "Failed to query KeyServer");
+ // status.putInt(Constants.extras.STATUS, Id.message.done);
+ // }
+ // }
+ // };
+ //
+ // t.setName("KeyExchange Download Thread");
+ // t.setDaemon(true);
+ // t.start();
+ // }
+ // }
+
+ public void scanAgainOnClick(View view) {
+ new IntentIntegrator(this).initiateScan();
+ }
+
+ public void finishOnClick(View view) {
+ finish();
+ }
+
+ public void importOnClick(View view) {
+ Log.d(Constants.TAG, "Import key button clicked!");
+
+ importKeys();
+ }
+
+ /**
+ * Import keys with mImportData
+ */
+ public void importKeys() {
+ if (mImportData != null || mImportFilename != null) {
+ Log.d(Constants.TAG, "importKeys started");
+
+ // Send all information needed to service to import key in other thread
+ Intent intent = new Intent(this, KeychainIntentService.class);
+
+ intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_IMPORT_KEYRING);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ // TODO: check for key type?
+ // data.putInt(ApgIntentService.IMPORT_KEY_TYPE, Id.type.secret_key);
+
+ if (mImportData != null) {
+ data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_BYTES);
+ data.putByteArray(KeychainIntentService.IMPORT_BYTES, mImportData);
+ } else {
+ data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_FILE);
+ data.putString(KeychainIntentService.IMPORT_FILENAME, mImportFilename);
+ }
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after importing is done in ApgService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
+ R.string.progress_importing, ProgressDialog.STYLE_HORIZONTAL) {
+ public void handleMessage(Message message) {
+ // handle messages by standard ApgHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ // get returned data bundle
+ Bundle returnData = message.getData();
+
+ int added = returnData.getInt(KeychainIntentService.RESULT_IMPORT_ADDED);
+ int updated = returnData.getInt(KeychainIntentService.RESULT_IMPORT_UPDATED);
+ int bad = returnData.getInt(KeychainIntentService.RESULT_IMPORT_BAD);
+ String toastMessage;
+ if (added > 0 && updated > 0) {
+ toastMessage = getString(R.string.keysAddedAndUpdated, added, updated);
+ } else if (added > 0) {
+ toastMessage = getString(R.string.keysAdded, added);
+ } else if (updated > 0) {
+ toastMessage = getString(R.string.keysUpdated, updated);
+ } else {
+ toastMessage = getString(R.string.noKeysAddedOrUpdated);
+ }
+ Toast.makeText(ImportKeysActivity.this, toastMessage, Toast.LENGTH_SHORT)
+ .show();
+ if (bad > 0) {
+ AlertDialog.Builder alert = new AlertDialog.Builder(
+ ImportKeysActivity.this);
+
+ alert.setIcon(android.R.drawable.ic_dialog_alert);
+ alert.setTitle(R.string.warning);
+ alert.setMessage(ImportKeysActivity.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
+ DeleteFileDialogFragment deleteFileDialog = DeleteFileDialogFragment
+ .newInstance(mImportFilename);
+ deleteFileDialog.show(getSupportFragmentManager(), "deleteDialog");
+ }
+ }
+ };
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // show progress dialog
+ saveHandler.showProgressDialog(this);
+
+ // start service with intent
+ startService(intent);
+ } else {
+ Toast.makeText(this, R.string.error_nothingImport, Toast.LENGTH_LONG).show();
+ }
+ }
+
+ public void signAndUploadOnClick(View view) {
+ // first, import!
+ importOnClick(view);
+
+ // TODO: implement sign and upload!
+ Toast.makeText(ImportKeysActivity.this, "Not implemented right now!", Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.request.filename: {
+ if (resultCode == RESULT_OK && data != null) {
+ try {
+ String path = data.getData().getPath();
+ Log.d(Constants.TAG, "path=" + path);
+
+ // set filename used in export/import dialogs
+ mFileDialog.setFilename(path);
+ } catch (NullPointerException e) {
+ Log.e(Constants.TAG, "Nullpointer while retrieving path!", e);
+ }
+ }
+ return;
+ }
+ case IntentIntegrator.REQUEST_CODE: {
+ IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode,
+ data);
+ if (scanResult != null && scanResult.getFormatName() != null) {
+
+ // mScannedContent = scanResult.getContents();
+
+ mImportData = scanResult.getContents().getBytes();
+ mImportFilename = null;
+
+ // mContentView.setText(mScannedContent);
+ // String[] bits = 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;
+ }
+
+ default: {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ loadKeyListFragment();
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java
new file mode 100644
index 000000000..170b459be
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * 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.sufficientlysecure.keychain.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.ui.widget.ImportKeysListLoader;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.SherlockListFragment;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v4.content.Loader;
+import android.support.v4.app.LoaderManager;
+import android.view.View;
+import android.widget.ListView;
+import android.widget.SimpleAdapter;
+
+public class ImportKeysListFragment extends SherlockListFragment implements
+ LoaderManager.LoaderCallbacks<List<Map<String, String>>> {
+ public static String ARG_KEYRING_BYTES = "bytes";
+ public static String ARG_IMPORT_FILENAME = "filename";
+
+ byte[] mKeyringBytes;
+ String mImportFilename;
+
+ private Activity mActivity;
+ private SimpleAdapter mAdapter;
+
+ @Override
+ public void onListItemClick(ListView listView, View view, int position, long id) {
+ // Map<String, String> item = (Map<String, String>) listView.getItemAtPosition(position);
+ // String userId = item.get(ImportKeysListLoader.MAP_ATTR_USER_ID);
+ }
+
+ /**
+ * Resume is called after rotating
+ */
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ // Start out with a progress indicator.
+ setListShown(false);
+
+ // reload list
+ getLoaderManager().restartLoader(0, null, this);
+ }
+
+ /**
+ * Define Adapter and Loader on create of Activity
+ */
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mActivity = this.getActivity();
+
+ mKeyringBytes = getArguments().getByteArray(ARG_KEYRING_BYTES);
+ mImportFilename = getArguments().getString(ARG_IMPORT_FILENAME);
+
+ // register long press context menu
+ registerForContextMenu(getListView());
+
+ // Give some text to display if there is no data. In a real
+ // application this would come from a resource.
+ setEmptyText(mActivity.getString(R.string.error_nothingImport));
+
+ // Create an empty adapter we will use to display the loaded data.
+ String[] from = new String[] {};
+ int[] to = new int[] {};
+ List<Map<String, String>> data = new ArrayList<Map<String, String>>();
+ mAdapter = new SimpleAdapter(getActivity(), data, android.R.layout.two_line_list_item,
+ from, to);
+ setListAdapter(mAdapter);
+
+ // Start out with a progress indicator.
+ setListShown(false);
+
+ // Prepare the loader. Either re-connect with an existing one,
+ // or start a new one.
+ getLoaderManager().initLoader(0, null, this);
+ }
+
+ @Override
+ public Loader<List<Map<String, String>>> onCreateLoader(int id, Bundle args) {
+ return new ImportKeysListLoader(mActivity, mKeyringBytes, mImportFilename);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<List<Map<String, String>>> loader,
+ List<Map<String, String>> data) {
+ // Set the new data in the adapter.
+ // for (String item : data) {
+ // mAdapter.add(item);
+ // }
+ Log.d(Constants.TAG, "data: " + data);
+ // TODO: real swapping the data to the already defined adapter doesn't work
+ // Workaround: recreate adapter!
+ // http://stackoverflow.com/questions/2356091/android-add-function-of-arrayadapter-not-working
+ // mAdapter = new ArrayAdapter<String>(mActivity, android.R.layout.two_line_list_item,
+ // data);
+
+ String[] from = new String[] { ImportKeysListLoader.MAP_ATTR_USER_ID,
+ ImportKeysListLoader.MAP_ATTR_FINGERPINT };
+ int[] to = new int[] { android.R.id.text1, android.R.id.text2 };
+ mAdapter = new SimpleAdapter(getActivity(), data, android.R.layout.two_line_list_item,
+ from, to);
+
+ mAdapter.notifyDataSetChanged();
+ setListAdapter(mAdapter);
+
+ // The list should now be shown.
+ if (isResumed()) {
+ setListShown(true);
+ } else {
+ setListShownNoAnimation(true);
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<List<Map<String, String>>> loader) {
+ // Clear the data in the adapter.
+ // Not available in SimpleAdapter!
+ // mAdapter.clear();
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListActivity.java
new file mode 100644
index 000000000..a128263c1
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListActivity.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * 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.sufficientlysecure.keychain.ui;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment;
+import org.sufficientlysecure.keychain.ui.dialog.FileDialogFragment;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import android.app.ProgressDialog;
+import android.app.SearchManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Messenger;
+import android.widget.Toast;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+public class KeyListActivity extends SherlockFragmentActivity {
+
+ protected String mExportFilename = Constants.path.APP_DIR + "/";
+
+ protected String mImportData;
+ protected boolean mDeleteAfterImport = false;
+
+ protected int mKeyType;
+
+ FileDialogFragment mFileDialog;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setHomeButtonEnabled(true);
+
+ handleActions(getIntent());
+ }
+
+ // TODO: needed?
+ // @Override
+ // protected void onNewIntent(Intent intent) {
+ // super.onNewIntent(intent);
+ // handleActions(intent);
+ // }
+
+ protected void handleActions(Intent intent) {
+ String action = intent.getAction();
+ Bundle extras = intent.getExtras();
+
+ if (extras == null) {
+ extras = new Bundle();
+ }
+
+ /**
+ * Android Standard Actions
+ */
+ String searchString = null;
+ if (Intent.ACTION_SEARCH.equals(action)) {
+ searchString = extras.getString(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);
+
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.request.filename: {
+ if (resultCode == RESULT_OK && data != null) {
+ try {
+ String path = data.getData().getPath();
+ Log.d(Constants.TAG, "path=" + path);
+
+ // set filename used in export/import dialogs
+ mFileDialog.setFilename(path);
+ } catch (NullPointerException e) {
+ Log.e(Constants.TAG, "Nullpointer while retrieving path!", e);
+ }
+ }
+ return;
+ }
+
+ default: {
+ break;
+ }
+ }
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ // TODO: reimplement!
+ // menu.add(3, Id.menu.option.search, 0, R.string.menu_search)
+ // .setIcon(R.drawable.ic_menu_search).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+ menu.add(0, Id.menu.option.import_from_file, 5, R.string.menu_importFromFile)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ menu.add(0, Id.menu.option.export_keys, 6, R.string.menu_exportKeys).setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_NEVER | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go home
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+
+ case Id.menu.option.import_from_file: {
+ Intent intentImportFromFile = new Intent(this, ImportKeysActivity.class);
+ intentImportFromFile.setAction(ImportKeysActivity.ACTION_IMPORT_FROM_FILE);
+ startActivityForResult(intentImportFromFile, 0);
+ return true;
+ }
+
+ case Id.menu.option.export_keys: {
+ showExportKeysDialog(-1);
+ return true;
+ }
+
+ // case Id.menu.option.search:
+ // startSearch("", false, null, false);
+ // return true;
+
+ default: {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+ }
+
+ /**
+ * Show dialog where to export keys
+ *
+ * @param keyRingMasterKeyId
+ * if -1 export all keys
+ */
+ public void showExportKeysDialog(final long keyRingMasterKeyId) {
+ // Message is received after file is selected
+ Handler returnHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == FileDialogFragment.MESSAGE_OKAY) {
+ Bundle data = message.getData();
+ mExportFilename = data.getString(FileDialogFragment.MESSAGE_DATA_FILENAME);
+
+ exportKeys(keyRingMasterKeyId);
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ final Messenger messenger = new Messenger(returnHandler);
+
+ DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() {
+ public void run() {
+ String title = null;
+ if (keyRingMasterKeyId != -1) {
+ // single key export
+ title = getString(R.string.title_exportKey);
+ } else {
+ title = getString(R.string.title_exportKeys);
+ }
+
+ String message = null;
+ if (mKeyType == Id.type.public_key) {
+ message = getString(R.string.specifyFileToExportTo);
+ } else {
+ message = getString(R.string.specifyFileToExportSecretKeysTo);
+ }
+
+ mFileDialog = FileDialogFragment.newInstance(messenger, title, message,
+ mExportFilename, null, Id.request.filename);
+
+ mFileDialog.show(getSupportFragmentManager(), "fileDialog");
+ }
+ });
+ }
+
+ /**
+ * Show dialog to delete key
+ *
+ * @param keyRingId
+ */
+ public void showDeleteKeyDialog(long keyRingId) {
+ // Message is received after key is deleted
+ Handler returnHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == DeleteKeyDialogFragment.MESSAGE_OKAY) {
+ // no further actions needed
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(returnHandler);
+
+ DeleteKeyDialogFragment deleteKeyDialog = DeleteKeyDialogFragment.newInstance(messenger,
+ keyRingId, mKeyType);
+
+ deleteKeyDialog.show(getSupportFragmentManager(), "deleteKeyDialog");
+ }
+
+ /**
+ * Export keys
+ *
+ * @param keyRingMasterKeyId
+ * if -1 export all keys
+ */
+ public void exportKeys(long keyRingMasterKeyId) {
+ Log.d(Constants.TAG, "exportKeys started");
+
+ // Send all information needed to service to export key in other thread
+ Intent intent = new Intent(this, KeychainIntentService.class);
+
+ intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_EXPORT_KEYRING);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ data.putString(KeychainIntentService.EXPORT_FILENAME, mExportFilename);
+ data.putInt(KeychainIntentService.EXPORT_KEY_TYPE, mKeyType);
+
+ if (keyRingMasterKeyId == -1) {
+ data.putBoolean(KeychainIntentService.EXPORT_ALL, true);
+ } else {
+ data.putLong(KeychainIntentService.EXPORT_KEY_RING_MASTER_KEY_ID, keyRingMasterKeyId);
+ }
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after exporting is done in ApgService
+ KeychainIntentServiceHandler exportHandler = new KeychainIntentServiceHandler(this,
+ R.string.progress_exporting, ProgressDialog.STYLE_HORIZONTAL) {
+ public void handleMessage(Message message) {
+ // handle messages by standard ApgHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ // get returned data bundle
+ Bundle returnData = message.getData();
+
+ int exported = returnData.getInt(KeychainIntentService.RESULT_EXPORT);
+ String toastMessage;
+ if (exported == 1) {
+ toastMessage = getString(R.string.keyExported);
+ } else if (exported > 0) {
+ toastMessage = getString(R.string.keysExported, exported);
+ } else {
+ toastMessage = getString(R.string.noKeysExported);
+ }
+ Toast.makeText(KeyListActivity.this, toastMessage, Toast.LENGTH_SHORT).show();
+
+ }
+ };
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(exportHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // show progress dialog
+ exportHandler.showProgressDialog(this);
+
+ // start service with intent
+ startService(intent);
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListFragment.java
new file mode 100644
index 000000000..9e90e254c
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListFragment.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * 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.sufficientlysecure.keychain.ui;
+
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.ui.widget.ExpandableListFragment;
+import org.sufficientlysecure.keychain.R;
+
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.View;
+import android.widget.ExpandableListView;
+import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
+
+public class KeyListFragment extends ExpandableListFragment {
+ protected KeyListActivity mKeyListActivity;
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mKeyListActivity = (KeyListActivity) getActivity();
+
+ // register long press context menu
+ registerForContextMenu(getListView());
+
+ // Give some text to display if there is no data. In a real
+ // application this would come from a resource.
+ setEmptyText(getString(R.string.listEmpty));
+ }
+
+ /**
+ * Context Menu on Long Click
+ */
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ menu.add(0, Id.menu.export, 5, R.string.menu_exportKey);
+ menu.add(0, Id.menu.delete, 111, R.string.menu_deleteKey);
+ }
+
+ @Override
+ public boolean onContextItemSelected(android.view.MenuItem item) {
+ ExpandableListContextMenuInfo expInfo = (ExpandableListContextMenuInfo) item.getMenuInfo();
+
+ // expInfo.id would also return row id of childs, but we always want to get the row id of
+ // the group item, thus we are using the following way
+ int groupPosition = ExpandableListView.getPackedPositionGroup(expInfo.packedPosition);
+ long keyRingRowId = getExpandableListAdapter().getGroupId(groupPosition);
+
+ switch (item.getItemId()) {
+ case Id.menu.export:
+ long masterKeyId = ProviderHelper.getPublicMasterKeyId(mKeyListActivity, keyRingRowId);
+ if (masterKeyId == -1) {
+ masterKeyId = ProviderHelper.getSecretMasterKeyId(mKeyListActivity, keyRingRowId);
+ }
+
+ mKeyListActivity.showExportKeysDialog(masterKeyId);
+ return true;
+
+ case Id.menu.delete:
+ mKeyListActivity.showDeleteKeyDialog(keyRingRowId);
+ return true;
+
+ default:
+ return super.onContextItemSelected(item);
+
+ }
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicActivity.java
new file mode 100644
index 000000000..adc8656d0
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicActivity.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * 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.sufficientlysecure.keychain.ui;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+public class KeyListPublicActivity extends KeyListActivity {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mKeyType = Id.type.public_key;
+
+ setContentView(R.layout.key_list_public_activity);
+
+ mExportFilename = Constants.path.APP_DIR + "/pubexport.asc";
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ menu.add(1, Id.menu.option.key_server, 1, R.string.menu_keyServer)
+ .setIcon(R.drawable.ic_menu_search_list)
+ .setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS);
+ menu.add(1, Id.menu.option.import_from_qr_code, 2, R.string.menu_importFromQrCode)
+ .setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ menu.add(1, Id.menu.option.import_from_nfc, 3, R.string.menu_importFromNfc)
+ .setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case Id.menu.option.key_server: {
+ startActivityForResult(new Intent(this, KeyServerQueryActivity.class), 0);
+
+ return true;
+ }
+ case Id.menu.option.import_from_file: {
+ Intent intentImportFromFile = new Intent(this, ImportKeysActivity.class);
+ intentImportFromFile.setAction(ImportKeysActivity.ACTION_IMPORT_FROM_FILE);
+ startActivityForResult(intentImportFromFile, 0);
+
+ return true;
+ }
+
+ case Id.menu.option.import_from_qr_code: {
+ Intent intentImportFromFile = new Intent(this, ImportKeysActivity.class);
+ intentImportFromFile.setAction(ImportKeysActivity.ACTION_IMPORT_FROM_QR_CODE);
+ startActivityForResult(intentImportFromFile, Id.request.import_from_qr_code);
+
+ return true;
+ }
+
+ case Id.menu.option.import_from_nfc: {
+ Intent intentImportFromFile = new Intent(this, ImportKeysActivity.class);
+ intentImportFromFile.setAction(ImportKeysActivity.ACTION_IMPORT_FROM_NFC);
+ startActivityForResult(intentImportFromFile, 0);
+
+ 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(KeyServerQueryActivity.RESULT_EXTRA_TEXT) == null) {
+ // return;
+ // }
+ //
+ // Intent intent = new Intent(this, KeyListPublicActivity.class);
+ // intent.setAction(KeyListPublicActivity.ACTION_IMPORT);
+ // intent.putExtra(KeyListPublicActivity.EXTRA_TEXT,
+ // data.getStringExtra(KeyListActivity.EXTRA_TEXT));
+ // handleActions(intent);
+ // break;
+ // }
+ //
+ // default: {
+ // super.onActivityResult(requestCode, resultCode, data);
+ // break;
+ // }
+ // }
+ // }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java
new file mode 100644
index 000000000..648b09ef7
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * 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.sufficientlysecure.keychain.ui;
+
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
+import org.sufficientlysecure.keychain.ui.widget.KeyListAdapter;
+import org.sufficientlysecure.keychain.R;
+
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.support.v4.app.LoaderManager;
+import android.view.ContextMenu;
+import android.view.View;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.ExpandableListView;
+import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
+
+public class KeyListPublicFragment extends KeyListFragment implements
+ LoaderManager.LoaderCallbacks<Cursor> {
+
+ private KeyListPublicActivity mKeyListPublicActivity;
+
+ private KeyListAdapter mAdapter;
+
+ /**
+ * Define Adapter and Loader on create of Activity
+ */
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mKeyListPublicActivity = (KeyListPublicActivity) getActivity();
+
+ mAdapter = new KeyListAdapter(mKeyListPublicActivity, null, Id.type.public_key);
+ setListAdapter(mAdapter);
+
+ // Start out with a progress indicator.
+ setListShown(false);
+
+ // Prepare the loader. Either re-connect with an existing one,
+ // or start a new one.
+ // id is -1 as the child cursors are numbered 0,...,n
+ getLoaderManager().initLoader(-1, null, this);
+ }
+
+ /**
+ * Context Menu on Long Click
+ */
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ menu.add(0, Id.menu.update, 1, R.string.menu_updateKey);
+ menu.add(0, Id.menu.signKey, 2, R.string.menu_signKey);
+ menu.add(0, Id.menu.exportToServer, 3, R.string.menu_exportKeyToServer);
+ menu.add(0, Id.menu.share, 6, R.string.menu_share);
+ menu.add(0, Id.menu.share_qr_code, 7, R.string.menu_shareQrCode);
+ menu.add(0, Id.menu.share_nfc, 8, R.string.menu_shareNfc);
+
+ }
+
+ @Override
+ public boolean onContextItemSelected(android.view.MenuItem item) {
+ ExpandableListContextMenuInfo expInfo = (ExpandableListContextMenuInfo) item.getMenuInfo();
+
+ // expInfo.id would also return row id of childs, but we always want to get the row id of
+ // the group item, thus we are using the following way
+ int groupPosition = ExpandableListView.getPackedPositionGroup(expInfo.packedPosition);
+ long keyRingRowId = getExpandableListAdapter().getGroupId(groupPosition);
+
+ switch (item.getItemId()) {
+ case Id.menu.update:
+ long updateKeyId = 0;
+ PGPPublicKeyRing updateKeyRing = ProviderHelper.getPGPPublicKeyRingByRowId(
+ mKeyListActivity, keyRingRowId);
+ if (updateKeyRing != null) {
+ updateKeyId = PgpHelper.getMasterKey(updateKeyRing).getKeyID();
+ }
+ if (updateKeyId == 0) {
+ // this shouldn't happen
+ return true;
+ }
+
+ Intent queryIntent = new Intent(mKeyListActivity, KeyServerQueryActivity.class);
+ queryIntent.setAction(KeyServerQueryActivity.ACTION_LOOK_UP_KEY_ID_AND_RETURN);
+ queryIntent.putExtra(KeyServerQueryActivity.EXTRA_KEY_ID, updateKeyId);
+
+ // TODO: lookup??
+ startActivityForResult(queryIntent, Id.request.look_up_key_id);
+
+ return true;
+
+ case Id.menu.exportToServer:
+ Intent uploadIntent = new Intent(mKeyListActivity, KeyServerUploadActivity.class);
+ uploadIntent.setAction(KeyServerUploadActivity.ACTION_EXPORT_KEY_TO_SERVER);
+ uploadIntent.putExtra(KeyServerUploadActivity.EXTRA_KEYRING_ROW_ID, keyRingRowId);
+ startActivityForResult(uploadIntent, Id.request.export_to_server);
+
+ return true;
+
+ case Id.menu.signKey:
+ long keyId = 0;
+ PGPPublicKeyRing signKeyRing = ProviderHelper.getPGPPublicKeyRingByRowId(
+ mKeyListActivity, keyRingRowId);
+ if (signKeyRing != null) {
+ keyId = PgpHelper.getMasterKey(signKeyRing).getKeyID();
+ }
+ if (keyId == 0) {
+ // this shouldn't happen
+ return true;
+ }
+
+ Intent signIntent = new Intent(mKeyListActivity, SignKeyActivity.class);
+ signIntent.putExtra(SignKeyActivity.EXTRA_KEY_ID, keyId);
+ startActivity(signIntent);
+
+ return true;
+
+ case Id.menu.share_qr_code:
+ // get master key id using row id
+ long masterKeyId = ProviderHelper.getPublicMasterKeyId(mKeyListActivity, keyRingRowId);
+
+ Intent qrCodeIntent = new Intent(mKeyListActivity, ShareActivity.class);
+ qrCodeIntent.setAction(ShareActivity.ACTION_SHARE_KEYRING_WITH_QR_CODE);
+ qrCodeIntent.putExtra(ShareActivity.EXTRA_MASTER_KEY_ID, masterKeyId);
+ startActivityForResult(qrCodeIntent, 0);
+
+ return true;
+
+ case Id.menu.share_nfc:
+ // get master key id using row id
+ long masterKeyId2 = ProviderHelper.getPublicMasterKeyId(mKeyListActivity, keyRingRowId);
+
+ Intent nfcIntent = new Intent(mKeyListActivity, ShareNfcBeamActivity.class);
+ nfcIntent.setAction(ShareNfcBeamActivity.ACTION_SHARE_KEYRING_WITH_NFC);
+ nfcIntent.putExtra(ShareNfcBeamActivity.EXTRA_MASTER_KEY_ID, masterKeyId2);
+ startActivityForResult(nfcIntent, 0);
+
+ return true;
+
+ case Id.menu.share:
+ // get master key id using row id
+ long masterKeyId3 = ProviderHelper.getPublicMasterKeyId(mKeyListActivity, keyRingRowId);
+
+ Intent shareIntent = new Intent(mKeyListActivity, ShareActivity.class);
+ shareIntent.setAction(ShareActivity.ACTION_SHARE_KEYRING);
+ shareIntent.putExtra(ShareActivity.EXTRA_MASTER_KEY_ID, masterKeyId3);
+ startActivityForResult(shareIntent, 0);
+
+ return true;
+
+ default:
+ return super.onContextItemSelected(item);
+
+ }
+ }
+
+ // These are the rows that we will retrieve.
+ static final String[] PROJECTION = new String[] { KeyRings._ID, KeyRings.MASTER_KEY_ID,
+ UserIds.USER_ID };
+
+ static final String SORT_ORDER = UserIds.USER_ID + " ASC";
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ // This is called when a new Loader needs to be created. This
+ // sample only has one Loader, so we don't care about the ID.
+ Uri baseUri = KeyRings.buildPublicKeyRingsUri();
+
+ // Now create and return a CursorLoader that will take care of
+ // creating a Cursor for the data being displayed.
+ return new CursorLoader(getActivity(), baseUri, PROJECTION, null, null, SORT_ORDER);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ // Swap the new cursor in. (The framework will take care of closing the
+ // old cursor once we return.)
+ mAdapter.setGroupCursor(data);
+
+ // The list should now be shown.
+ if (isResumed()) {
+ setListShown(true);
+ } else {
+ setListShownNoAnimation(true);
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ // This is called when the last Cursor provided to onLoadFinished()
+ // above is about to be closed. We need to make sure we are no
+ // longer using it.
+ mAdapter.setGroupCursor(null);
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretActivity.java
new file mode 100644
index 000000000..63d06edaf
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretActivity.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * 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.sufficientlysecure.keychain.ui;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.PgpMain;
+import org.sufficientlysecure.keychain.service.PassphraseCacheService;
+import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Messenger;
+
+public class KeyListSecretActivity extends KeyListActivity {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mKeyType = Id.type.secret_key;
+
+ setContentView(R.layout.key_list_secret_activity);
+
+ mExportFilename = Constants.path.APP_DIR + "/secexport.asc";
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ menu.add(1, Id.menu.option.create, 1, R.string.menu_createKey).setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case Id.menu.option.create: {
+ createKey();
+ return true;
+ }
+
+ default: {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+ }
+
+ public void checkPassPhraseAndEdit(long keyId) {
+ String passPhrase = PassphraseCacheService.getCachedPassphrase(this, keyId);
+ if (passPhrase == null) {
+ showPassphraseDialog(keyId);
+ } else {
+ PgpMain.setEditPassPhrase(passPhrase);
+ editKey(keyId);
+ }
+ }
+
+ private void showPassphraseDialog(final long secretKeyId) {
+ // Message is received after passphrase is cached
+ Handler returnHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) {
+ String passPhrase = PassphraseCacheService.getCachedPassphrase(
+ KeyListSecretActivity.this, secretKeyId);
+ PgpMain.setEditPassPhrase(passPhrase);
+ editKey(secretKeyId);
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(returnHandler);
+
+ try {
+ PassphraseDialogFragment passphraseDialog = PassphraseDialogFragment.newInstance(
+ KeyListSecretActivity.this, messenger, secretKeyId);
+
+ passphraseDialog.show(getSupportFragmentManager(), "passphraseDialog");
+ } catch (PgpMain.PgpGeneralException e) {
+ Log.d(Constants.TAG, "No passphrase for this secret key, encrypt directly!");
+ // send message to handler to start encryption directly
+ returnHandler.sendEmptyMessage(PassphraseDialogFragment.MESSAGE_OKAY);
+ }
+ }
+
+ private void createKey() {
+ PgpMain.setEditPassPhrase("");
+ Intent intent = new Intent(EditKeyActivity.ACTION_CREATE_KEY);
+ startActivityForResult(intent, 0);
+ }
+
+ private void editKey(long keyId) {
+ Intent intent = new Intent(EditKeyActivity.ACTION_EDIT_KEY);
+ intent.putExtra(EditKeyActivity.EXTRA_KEY_ID, keyId);
+ startActivityForResult(intent, 0);
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretFragment.java
new file mode 100644
index 000000000..f4ba2b4d9
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretFragment.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * 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.sufficientlysecure.keychain.ui;
+
+import java.util.ArrayList;
+
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
+import org.sufficientlysecure.keychain.ui.widget.KeyListAdapter;
+import org.sufficientlysecure.keychain.R;
+
+import com.google.zxing.integration.android.IntentIntegrator;
+
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.support.v4.app.LoaderManager;
+import android.view.ContextMenu;
+import android.view.View;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.ExpandableListView;
+import android.widget.Toast;
+import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
+
+public class KeyListSecretFragment extends KeyListFragment implements
+ LoaderManager.LoaderCallbacks<Cursor> {
+
+ private KeyListSecretActivity mKeyListSecretActivity;
+
+ private KeyListAdapter mAdapter;
+
+ /**
+ * Define Adapter and Loader on create of Activity
+ */
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mKeyListSecretActivity = (KeyListSecretActivity) getActivity();
+
+ mAdapter = new KeyListAdapter(mKeyListSecretActivity, null, Id.type.secret_key);
+ setListAdapter(mAdapter);
+
+ // Start out with a progress indicator.
+ setListShown(false);
+
+ // Prepare the loader. Either re-connect with an existing one,
+ // or start a new one.
+ // id is -1 as the child cursors are numbered 0,...,n
+ getLoaderManager().initLoader(-1, null, this);
+ }
+
+ /**
+ * Context Menu on Long Click
+ */
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ menu.add(0, Id.menu.edit, 0, R.string.menu_editKey);
+ }
+
+ @Override
+ public boolean onContextItemSelected(android.view.MenuItem item) {
+ ExpandableListContextMenuInfo expInfo = (ExpandableListContextMenuInfo) item.getMenuInfo();
+
+ // expInfo.id would also return row id of childs, but we always want to get the row id of
+ // the group item, thus we are using the following way
+ int groupPosition = ExpandableListView.getPackedPositionGroup(expInfo.packedPosition);
+ long keyRingRowId = getExpandableListAdapter().getGroupId(groupPosition);
+
+ // get master key id using row id
+ long masterKeyId = ProviderHelper
+ .getSecretMasterKeyId(mKeyListSecretActivity, keyRingRowId);
+
+ switch (item.getItemId()) {
+ case Id.menu.edit:
+ mKeyListSecretActivity.checkPassPhraseAndEdit(masterKeyId);
+
+ return true;
+
+ default:
+ return super.onContextItemSelected(item);
+
+ }
+ }
+
+
+ // These are the rows that we will retrieve.
+ static final String[] PROJECTION = new String[] { KeyRings._ID, KeyRings.MASTER_KEY_ID,
+ UserIds.USER_ID };
+
+ static final String SORT_ORDER = UserIds.USER_ID + " ASC";
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ // This is called when a new Loader needs to be created. This
+ // sample only has one Loader, so we don't care about the ID.
+ Uri baseUri = KeyRings.buildSecretKeyRingsUri();
+
+ // Now create and return a CursorLoader that will take care of
+ // creating a Cursor for the data being displayed.
+ return new CursorLoader(getActivity(), baseUri, PROJECTION, null, null, SORT_ORDER);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ // Swap the new cursor in. (The framework will take care of closing the
+ // old cursor once we return.)
+ mAdapter.setGroupCursor(data);
+
+ // The list should now be shown.
+ if (isResumed()) {
+ setListShown(true);
+ } else {
+ setListShownNoAnimation(true);
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ // This is called when the last Cursor provided to onLoadFinished()
+ // above is about to be closed. We need to make sure we are no
+ // longer using it.
+ mAdapter.setGroupCursor(null);
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerQueryActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerQueryActivity.java
new file mode 100644
index 000000000..df8b7b374
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerQueryActivity.java
@@ -0,0 +1,353 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * 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.sufficientlysecure.keychain.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.helper.Preferences;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.util.KeyServer.KeyInfo;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.Messenger;
+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 SherlockFragmentActivity {
+
+ // possible intent actions for this activity
+ public static final String ACTION_LOOK_UP_KEY_ID = Constants.INTENT_PREFIX + "LOOK_UP_KEY_ID";
+ public static final String ACTION_LOOK_UP_KEY_ID_AND_RETURN = Constants.INTENT_PREFIX
+ + "LOOK_UP_KEY_ID_AND_RETURN";
+
+ public static final String EXTRA_KEY_ID = "keyId";
+
+ public static final String RESULT_EXTRA_TEXT = "text";
+
+ 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
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go home
+ Intent intent = new Intent(this, KeyListPublicActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+
+ default:
+ break;
+
+ }
+ return false;
+ }
+
+ @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, Preferences.getPreferences(this)
+ .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();
+ String action = intent.getAction();
+ if (ACTION_LOOK_UP_KEY_ID.equals(action) || ACTION_LOOK_UP_KEY_ID_AND_RETURN.equals(action)) {
+ long keyId = intent.getLongExtra(EXTRA_KEY_ID, 0);
+ if (keyId != 0) {
+ String query = "0x" + PgpHelper.keyToHex(keyId);
+ mQuery.setText(query);
+ search(query);
+ }
+ }
+ }
+
+ private void search(String query) {
+ mQueryType = Id.keyserver.search;
+ mQueryString = query;
+ mAdapter.setKeys(new ArrayList<KeyInfo>());
+
+ start();
+ }
+
+ private void get(long keyId) {
+ mQueryType = Id.keyserver.get;
+ mQueryId = keyId;
+
+ start();
+ }
+
+ private void start() {
+ Log.d(Constants.TAG, "start search with service");
+
+ // Send all information needed to service to query keys in other thread
+ Intent intent = new Intent(this, KeychainIntentService.class);
+
+ intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_QUERY_KEYRING);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ String server = (String) mKeyServer.getSelectedItem();
+ data.putString(KeychainIntentService.QUERY_KEY_SERVER, server);
+
+ data.putInt(KeychainIntentService.QUERY_KEY_TYPE, mQueryType);
+
+ if (mQueryType == Id.keyserver.search) {
+ data.putString(KeychainIntentService.QUERY_KEY_STRING, mQueryString);
+ } else if (mQueryType == Id.keyserver.get) {
+ data.putLong(KeychainIntentService.QUERY_KEY_ID, mQueryId);
+ }
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after querying is done in ApgService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
+ R.string.progress_querying, ProgressDialog.STYLE_SPINNER) {
+ public void handleMessage(Message message) {
+ // handle messages by standard ApgHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ // get returned data bundle
+ Bundle returnData = message.getData();
+
+ if (mQueryType == Id.keyserver.search) {
+ mSearchResult = returnData
+ .getParcelableArrayList(KeychainIntentService.RESULT_QUERY_KEY_SEARCH_RESULT);
+ } else if (mQueryType == Id.keyserver.get) {
+ mKeyData = returnData.getString(KeychainIntentService.RESULT_QUERY_KEY_KEY_DATA);
+ }
+
+ // TODO: IMPROVE CODE!!! some global variables can be avoided!!!
+ if (mQueryType == Id.keyserver.search) {
+ if (mSearchResult != null) {
+ Toast.makeText(KeyServerQueryActivity.this,
+ getString(R.string.keysFound, mSearchResult.size()),
+ Toast.LENGTH_SHORT).show();
+ mAdapter.setKeys(mSearchResult);
+ }
+ } else if (mQueryType == Id.keyserver.get) {
+ Intent orgIntent = getIntent();
+ if (ACTION_LOOK_UP_KEY_ID_AND_RETURN.equals(orgIntent.getAction())) {
+ if (mKeyData != null) {
+ Intent intent = new Intent();
+ intent.putExtra(RESULT_EXTRA_TEXT, mKeyData);
+ setResult(RESULT_OK, intent);
+ } else {
+ setResult(RESULT_CANCELED);
+ }
+ finish();
+ } else {
+ if (mKeyData != null) {
+ Intent intent = new Intent(KeyServerQueryActivity.this,
+ ImportKeysActivity.class);
+ intent.setAction(ImportKeysActivity.ACTION_IMPORT);
+ intent.putExtra(ImportKeysActivity.EXTRA_TEXT, mKeyData);
+ startActivity(intent);
+ }
+ }
+ }
+
+ }
+ };
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // show progress dialog
+ saveHandler.showProgressDialog(this);
+
+ // start service with intent
+ startService(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 ArrayList<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(PgpHelper.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.MATCH_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/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerUploadActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerUploadActivity.java
new file mode 100644
index 000000000..657044f2b
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerUploadActivity.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2011 Senecaso
+ *
+ * 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.sufficientlysecure.keychain.ui;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.helper.Preferences;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.app.ProgressDialog;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.Messenger;
+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 KeyServerUploadActivity extends SherlockFragmentActivity {
+
+ // Not used in sourcode, but listed in AndroidManifest!
+ public static final String ACTION_EXPORT_KEY_TO_SERVER = Constants.INTENT_PREFIX
+ + "EXPORT_KEY_TO_SERVER";
+
+ public static final String EXTRA_KEYRING_ROW_ID = "keyId";
+
+ private Button export;
+ private Spinner keyServer;
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go home
+ Intent intent = new Intent(this, KeyListPublicActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+
+ default:
+ break;
+
+ }
+ return false;
+ }
+
+ @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, Preferences.getPreferences(this)
+ .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) {
+ uploadKey();
+ }
+ });
+ }
+
+ private void uploadKey() {
+ // Send all information needed to service to upload key in other thread
+ Intent intent = new Intent(this, KeychainIntentService.class);
+
+ intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_UPLOAD_KEYRING);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ int keyRingId = getIntent().getIntExtra(EXTRA_KEYRING_ROW_ID, -1);
+ data.putInt(KeychainIntentService.UPLOAD_KEY_KEYRING_ROW_ID, keyRingId);
+
+ String server = (String) keyServer.getSelectedItem();
+ data.putString(KeychainIntentService.UPLOAD_KEY_SERVER, server);
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after uploading is done in ApgService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this, R.string.progress_exporting,
+ ProgressDialog.STYLE_HORIZONTAL) {
+ public void handleMessage(Message message) {
+ // handle messages by standard ApgHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+
+ Toast.makeText(KeyServerUploadActivity.this, R.string.keySendSuccess,
+ Toast.LENGTH_SHORT).show();
+ finish();
+ }
+ };
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // show progress dialog
+ saveHandler.showProgressDialog(this);
+
+ // start service with intent
+ startService(intent);
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/MainActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/MainActivity.java
new file mode 100644
index 000000000..447801e55
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/MainActivity.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * 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.sufficientlysecure.keychain.ui;
+
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.app.SherlockActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+
+public class MainActivity extends SherlockActivity {
+
+ public void manageKeysOnClick(View view) {
+ // used instead of startActivity set actionbar based on callingPackage
+ startActivityForResult(new Intent(this, KeyListPublicActivity.class), 0);
+ }
+
+ public void myKeysOnClick(View view) {
+ // used instead of startActivity set actionbar based on callingPackage
+ startActivityForResult(new Intent(this, KeyListSecretActivity.class), 0);
+ }
+
+ public void encryptOnClick(View view) {
+ Intent intent = new Intent(MainActivity.this, EncryptActivity.class);
+ intent.setAction(EncryptActivity.ACTION_ENCRYPT);
+ // used instead of startActivity set actionbar based on callingPackage
+ startActivityForResult(intent, 0);
+ }
+
+ public void decryptOnClick(View view) {
+ Intent intent = new Intent(MainActivity.this, DecryptActivity.class);
+ intent.setAction(DecryptActivity.ACTION_DECRYPT);
+ // used instead of startActivity set actionbar based on callingPackage
+ startActivityForResult(intent, 0);
+ }
+
+ public void scanQrcodeOnClick(View view) {
+ Intent intent = new Intent(this, ImportKeysActivity.class);
+ startActivityForResult(intent, 0);
+ }
+
+ public void helpOnClick(View view) {
+ startActivity(new Intent(this, HelpActivity.class));
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main);
+
+ final ActionBar actionBar = getSupportActionBar();
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(false);
+ actionBar.setHomeButtonEnabled(false);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, Id.menu.option.preferences, 0, R.string.menu_preferences)
+ .setIcon(R.drawable.ic_menu_settings)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case Id.menu.option.preferences:
+ startActivity(new Intent(this, PreferencesActivity.class));
+ return true;
+
+ default:
+ break;
+
+ }
+ return false;
+ }
+
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/PreferencesActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/PreferencesActivity.java
new file mode 100644
index 000000000..3c070c6db
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/PreferencesActivity.java
@@ -0,0 +1,240 @@
+/*
+ * 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.sufficientlysecure.keychain.ui;
+
+import org.spongycastle.bcpg.HashAlgorithmTags;
+import org.spongycastle.openpgp.PGPEncryptedData;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.Preferences;
+import org.sufficientlysecure.keychain.ui.widget.IntegerListPreference;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.app.SherlockPreferenceActivity;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.Preference;
+import android.preference.PreferenceScreen;
+
+public class PreferencesActivity extends SherlockPreferenceActivity {
+ 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);
+ super.onCreate(savedInstanceState);
+
+ final ActionBar actionBar = getSupportActionBar();
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setHomeButtonEnabled(true);
+
+ addPreferencesFromResource(R.xml.preferences);
+
+ 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()));
+ 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,
+ PreferencesKeyServerActivity.class);
+ intent.putExtra(PreferencesKeyServerActivity.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(PreferencesKeyServerActivity.EXTRA_KEY_SERVERS);
+ mPreferences.setKeyServers(servers);
+ mKeyServerPreference.setSummary(getResources().getString(R.string.nKeyServers,
+ servers.length));
+ break;
+ }
+
+ default: {
+ super.onActivityResult(requestCode, resultCode, data);
+ break;
+ }
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go home
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+
+ default:
+ break;
+
+ }
+ return false;
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/PreferencesKeyServerActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/PreferencesKeyServerActivity.java
new file mode 100644
index 000000000..b86fe0a2e
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/PreferencesKeyServerActivity.java
@@ -0,0 +1,162 @@
+/*
+ * 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.sufficientlysecure.keychain.ui;
+
+import java.util.Vector;
+
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.ui.widget.Editor;
+import org.sufficientlysecure.keychain.ui.widget.KeyServerEditor;
+import org.sufficientlysecure.keychain.ui.widget.Editor.EditorListener;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.app.SherlockActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+public class PreferencesKeyServerActivity extends SherlockActivity implements OnClickListener,
+ EditorListener {
+
+ public static final String EXTRA_KEY_SERVERS = "keyServers";
+
+ private LayoutInflater mInflater;
+ private ViewGroup mEditors;
+ private View mAdd;
+ private TextView mTitle;
+ private TextView mSummary;
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go home
+ Intent intent = new Intent(this, PreferencesActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+
+ return true;
+
+ case Id.menu.option.okay:
+ okClicked();
+
+ return true;
+
+ case Id.menu.option.cancel:
+ cancelClicked();
+
+ return true;
+
+ default:
+ break;
+
+ }
+ return false;
+ }
+
+ /**
+ * ActionBar menu is created based on class variables to change it at runtime
+ *
+ */
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+
+ menu.add(1, Id.menu.option.cancel, 0, android.R.string.cancel).setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+
+ menu.add(1, Id.menu.option.okay, 1, android.R.string.ok).setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+
+ return true;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.key_server_preference);
+
+ final ActionBar actionBar = getSupportActionBar();
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setHomeButtonEnabled(true);
+
+ 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(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);
+ }
+ }
+ }
+
+ 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(EXTRA_KEY_SERVERS, servers.toArray(dummy));
+ setResult(RESULT_OK, data);
+ finish();
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectPublicKeyActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectPublicKeyActivity.java
new file mode 100644
index 000000000..fc1cfba17
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectPublicKeyActivity.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * 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.sufficientlysecure.keychain.ui;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+public class SelectPublicKeyActivity extends SherlockFragmentActivity {
+
+ // Not used in sourcode, but listed in AndroidManifest!
+ public static final String ACTION_SELECT_PUBLIC_KEYS = Constants.INTENT_PREFIX
+ + "SELECT_PUBLIC_KEYS";
+
+ public static final String EXTRA_SELECTED_MASTER_KEY_IDS = "masterKeyIds";
+
+ public static final String RESULT_EXTRA_MASTER_KEY_IDS = "masterKeyIds";
+ public static final String RESULT_EXTRA_USER_IDS = "userIds";
+
+ SelectPublicKeyFragment mSelectFragment;
+
+ long selectedMasterKeyIds[];
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.select_public_key_activity);
+
+ final ActionBar actionBar = getSupportActionBar();
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(false);
+ actionBar.setHomeButtonEnabled(false);
+
+ setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
+
+ mSelectFragment = (SelectPublicKeyFragment) getSupportFragmentManager().findFragmentById(
+ R.id.select_public_key_fragment);
+
+ // TODO: reimplement!
+ // 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) {
+ // TODO: reimplement!
+
+ // 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));
+ // }
+
+ // preselected master keys
+ selectedMasterKeyIds = intent.getLongArrayExtra(EXTRA_SELECTED_MASTER_KEY_IDS);
+ }
+
+ /**
+ * returns preselected key ids, this is used in the fragment
+ *
+ * @return
+ */
+ public long[] getSelectedMasterKeyIds() {
+ return selectedMasterKeyIds;
+ }
+
+ private void cancelClicked() {
+ setResult(RESULT_CANCELED, null);
+ finish();
+ }
+
+ private void okClicked() {
+ Intent data = new Intent();
+ data.putExtra(RESULT_EXTRA_MASTER_KEY_IDS, mSelectFragment.getSelectedMasterKeyIds());
+ data.putExtra(RESULT_EXTRA_USER_IDS, mSelectFragment.getSelectedUserIds());
+ setResult(RESULT_OK, data);
+ finish();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // TODO: reimplement!
+ // menu.add(0, Id.menu.option.search, 0, R.string.menu_search).setIcon(
+ // android.R.drawable.ic_menu_search);
+ menu.add(1, Id.menu.option.cancel, 0, R.string.btn_doNotSave).setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ menu.add(1, Id.menu.option.okay, 1, R.string.btn_okay).setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ return true;
+ }
+
+ /**
+ * Menu Options
+ */
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go home
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+
+ case Id.menu.option.okay:
+ okClicked();
+ return true;
+
+ case Id.menu.option.cancel:
+ cancelClicked();
+ return true;
+
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectPublicKeyFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectPublicKeyFragment.java
new file mode 100644
index 000000000..8b3e75d05
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectPublicKeyFragment.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * 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.sufficientlysecure.keychain.ui;
+
+import java.util.Date;
+import java.util.Vector;
+
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.compatibility.ListFragmentWorkaround;
+import org.sufficientlysecure.keychain.provider.KeychainDatabase;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
+import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
+import org.sufficientlysecure.keychain.ui.widget.SelectKeyCursorAdapter;
+import org.sufficientlysecure.keychain.R;
+
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.support.v4.app.LoaderManager;
+import android.widget.ListView;
+
+public class SelectPublicKeyFragment extends ListFragmentWorkaround implements
+ LoaderManager.LoaderCallbacks<Cursor> {
+
+ private SelectPublicKeyActivity mActivity;
+ private SelectKeyCursorAdapter mAdapter;
+ private ListView mListView;
+
+ private long mSelectedMasterKeyIds[];
+
+ /**
+ * Define Adapter and Loader on create of Activity
+ */
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mActivity = (SelectPublicKeyActivity) getSherlockActivity();
+ mListView = getListView();
+
+ // get selected master key ids, which are given to activity by intent
+ mSelectedMasterKeyIds = mActivity.getSelectedMasterKeyIds();
+
+ mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+
+ // Give some text to display if there is no data. In a real
+ // application this would come from a resource.
+ setEmptyText(getString(R.string.listEmpty));
+
+ mAdapter = new SelectKeyCursorAdapter(mActivity, mListView, null, Id.type.public_key);
+
+ setListAdapter(mAdapter);
+
+ // Start out with a progress indicator.
+ setListShown(false);
+
+ // Prepare the loader. Either re-connect with an existing one,
+ // or start a new one.
+ getLoaderManager().initLoader(0, null, this);
+ }
+
+ /**
+ * Selects items based on master key ids in list view
+ *
+ * @param masterKeyIds
+ */
+ private void preselectMasterKeyIds(long[] masterKeyIds) {
+ if (masterKeyIds != null) {
+ for (int i = 0; i < mListView.getCount(); ++i) {
+ long keyId = mAdapter.getMasterKeyId(i);
+ for (int j = 0; j < masterKeyIds.length; ++j) {
+ if (keyId == masterKeyIds[j]) {
+ mListView.setItemChecked(i, true);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns all selected master key ids
+ *
+ * @return
+ */
+ public long[] getSelectedMasterKeyIds() {
+ // mListView.getCheckedItemIds() would give the row ids of the KeyRings not the master key
+ // ids!
+ Vector<Long> vector = new Vector<Long>();
+ for (int i = 0; i < mListView.getCount(); ++i) {
+ if (mListView.isItemChecked(i)) {
+ vector.add(mAdapter.getMasterKeyId(i));
+ }
+ }
+
+ // convert to long array
+ long[] selectedMasterKeyIds = new long[vector.size()];
+ for (int i = 0; i < vector.size(); ++i) {
+ selectedMasterKeyIds[i] = vector.get(i);
+ }
+
+ return selectedMasterKeyIds;
+ }
+
+ /**
+ * Returns all selected user ids
+ *
+ * @return
+ */
+ public String[] getSelectedUserIds() {
+ Vector<String> userIds = new Vector<String>();
+ for (int i = 0; i < mListView.getCount(); ++i) {
+ if (mListView.isItemChecked(i)) {
+ userIds.add((String) mAdapter.getUserId(i));
+ }
+ }
+
+ // make empty array to not return null
+ String userIdArray[] = new String[0];
+ return userIds.toArray(userIdArray);
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ // This is called when a new Loader needs to be created. This
+ // sample only has one Loader, so we don't care about the ID.
+ Uri baseUri = KeyRings.buildPublicKeyRingsUri();
+
+ // These are the rows that we will retrieve.
+ long now = new Date().getTime() / 1000;
+ String[] projection = new String[] {
+ KeyRings._ID,
+ KeyRings.MASTER_KEY_ID,
+ UserIds.USER_ID,
+ "(SELECT COUNT(available_keys." + Keys._ID + ") FROM " + Tables.KEYS
+ + " AS available_keys WHERE available_keys." + Keys.KEY_RING_ROW_ID + " = "
+ + KeychainDatabase.Tables.KEY_RINGS + "." + KeyRings._ID
+ + " AND available_keys." + Keys.IS_REVOKED + " = '0' AND available_keys."
+ + Keys.CAN_ENCRYPT + " = '1') AS "
+ + SelectKeyCursorAdapter.PROJECTION_ROW_AVAILABLE,
+ "(SELECT COUNT(valid_keys." + Keys._ID + ") FROM " + Tables.KEYS
+ + " AS valid_keys WHERE valid_keys." + Keys.KEY_RING_ROW_ID + " = "
+ + KeychainDatabase.Tables.KEY_RINGS + "." + KeyRings._ID + " AND valid_keys."
+ + Keys.IS_REVOKED + " = '0' AND valid_keys." + Keys.CAN_ENCRYPT
+ + " = '1' AND valid_keys." + Keys.CREATION + " <= '" + now + "' AND "
+ + "(valid_keys." + Keys.EXPIRY + " IS NULL OR valid_keys." + Keys.EXPIRY
+ + " >= '" + now + "')) AS " + SelectKeyCursorAdapter.PROJECTION_ROW_VALID, };
+
+ String inMasterKeyList = null;
+ if (mSelectedMasterKeyIds != null && mSelectedMasterKeyIds.length > 0) {
+ inMasterKeyList = KeyRings.MASTER_KEY_ID + " IN (";
+ for (int i = 0; i < mSelectedMasterKeyIds.length; ++i) {
+ if (i != 0) {
+ inMasterKeyList += ", ";
+ }
+ inMasterKeyList += DatabaseUtils.sqlEscapeString("" + mSelectedMasterKeyIds[i]);
+ }
+ inMasterKeyList += ")";
+ }
+
+ // 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.USER_ID + " ASC";
+ if (inMasterKeyList != null) {
+ // sort by selected master keys
+ orderBy = inMasterKeyList + " DESC, " + orderBy;
+ }
+
+ // Now create and return a CursorLoader that will take care of
+ // creating a Cursor for the data being displayed.
+ return new CursorLoader(getActivity(), baseUri, projection, null, null, orderBy);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ // Swap the new cursor in. (The framework will take care of closing the
+ // old cursor once we return.)
+ mAdapter.swapCursor(data);
+
+ // The list should now be shown.
+ if (isResumed()) {
+ setListShown(true);
+ } else {
+ setListShownNoAnimation(true);
+ }
+
+ // preselect given master keys
+ preselectMasterKeyIds(mSelectedMasterKeyIds);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ // This is called when the last Cursor provided to onLoadFinished()
+ // above is about to be closed. We need to make sure we are no
+ // longer using it.
+ mAdapter.swapCursor(null);
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectSecretKeyActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectSecretKeyActivity.java
new file mode 100644
index 000000000..6f43cbabc
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectSecretKeyActivity.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * 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.sufficientlysecure.keychain.ui;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+public class SelectSecretKeyActivity extends SherlockFragmentActivity {
+
+ // Not used in sourcode, but listed in AndroidManifest!
+ public static final String ACTION_SELECT_SECRET_KEY = Constants.INTENT_PREFIX
+ + "SELECT_SECRET_KEY";
+
+ public static final String RESULT_EXTRA_MASTER_KEY_ID = "masterKeyId";
+ public static final String RESULT_EXTRA_USER_ID = "userId";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.select_secret_key_activity);
+
+ final ActionBar actionBar = getSupportActionBar();
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(false);
+ actionBar.setHomeButtonEnabled(false);
+
+ setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
+
+ // TODO: reimplement!
+ // 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());
+ }
+
+ /**
+ * This is executed by SelectSecretKeyFragment after clicking on an item
+ *
+ * @param masterKeyId
+ * @param userId
+ */
+ public void afterListSelection(long masterKeyId, String userId) {
+ Intent data = new Intent();
+ data.putExtra(RESULT_EXTRA_MASTER_KEY_ID, masterKeyId);
+ data.putExtra(RESULT_EXTRA_USER_ID, (String) userId);
+ setResult(RESULT_OK, data);
+ finish();
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ handleIntent(intent);
+ }
+
+ private void handleIntent(Intent intent) {
+ // TODO: reimplement!
+
+ // 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));
+ // }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // TODO: reimplement!
+ // menu.add(0, Id.menu.option.search, 0, R.string.menu_search).setIcon(
+ // android.R.drawable.ic_menu_search);
+ return true;
+ }
+
+ /**
+ * Menu Options
+ */
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go home
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectSecretKeyFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectSecretKeyFragment.java
new file mode 100644
index 000000000..d7d9fb0af
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectSecretKeyFragment.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * 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.sufficientlysecure.keychain.ui;
+
+import java.util.Date;
+
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.provider.KeychainDatabase;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
+import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
+import org.sufficientlysecure.keychain.ui.widget.SelectKeyCursorAdapter;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.SherlockListFragment;
+
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.support.v4.app.LoaderManager;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+
+public class SelectSecretKeyFragment extends SherlockListFragment implements
+ LoaderManager.LoaderCallbacks<Cursor> {
+
+ private SelectSecretKeyActivity mActivity;
+ private SelectKeyCursorAdapter mAdapter;
+ private ListView mListView;
+
+ /**
+ * Define Adapter and Loader on create of Activity
+ */
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mActivity = (SelectSecretKeyActivity) getSherlockActivity();
+ mListView = getListView();
+
+ mListView.setOnItemClickListener(new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
+ long masterKeyId = mAdapter.getMasterKeyId(position);
+ String userId = mAdapter.getUserId(position);
+
+ // return data to activity, which results in finishing it
+ mActivity.afterListSelection(masterKeyId, userId);
+ }
+ });
+
+ // Give some text to display if there is no data. In a real
+ // application this would come from a resource.
+ setEmptyText(getString(R.string.listEmpty));
+
+ mAdapter = new SelectKeyCursorAdapter(mActivity, mListView, null, Id.type.secret_key);
+
+ setListAdapter(mAdapter);
+
+ // Start out with a progress indicator.
+ setListShown(false);
+
+ // Prepare the loader. Either re-connect with an existing one,
+ // or start a new one.
+ getLoaderManager().initLoader(0, null, this);
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ // This is called when a new Loader needs to be created. This
+ // sample only has one Loader, so we don't care about the ID.
+ Uri baseUri = KeyRings.buildPublicKeyRingsUri();
+
+ // These are the rows that we will retrieve.
+ long now = new Date().getTime() / 1000;
+ String[] projection = new String[] {
+ KeyRings._ID,
+ KeyRings.MASTER_KEY_ID,
+ UserIds.USER_ID,
+ "(SELECT COUNT(available_keys." + Keys._ID + ") FROM " + Tables.KEYS
+ + " AS available_keys WHERE available_keys." + Keys.KEY_RING_ROW_ID + " = "
+ + KeychainDatabase.Tables.KEY_RINGS + "." + KeyRings._ID
+ + " AND available_keys." + Keys.IS_REVOKED + " = '0' AND available_keys."
+ + Keys.CAN_SIGN + " = '1') AS "
+ + SelectKeyCursorAdapter.PROJECTION_ROW_AVAILABLE,
+ "(SELECT COUNT(valid_keys." + Keys._ID + ") FROM " + Tables.KEYS
+ + " AS valid_keys WHERE valid_keys." + Keys.KEY_RING_ROW_ID + " = "
+ + KeychainDatabase.Tables.KEY_RINGS + "." + KeyRings._ID + " AND valid_keys."
+ + Keys.IS_REVOKED + " = '0' AND valid_keys." + Keys.CAN_SIGN
+ + " = '1' AND valid_keys." + Keys.CREATION + " <= '" + now + "' AND "
+ + "(valid_keys." + Keys.EXPIRY + " IS NULL OR valid_keys." + Keys.EXPIRY
+ + " >= '" + now + "')) AS " + SelectKeyCursorAdapter.PROJECTION_ROW_VALID, };
+
+ // 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(")");
+ // }
+
+ String orderBy = UserIds.USER_ID + " ASC";
+
+ // Now create and return a CursorLoader that will take care of
+ // creating a Cursor for the data being displayed.
+ return new CursorLoader(getActivity(), baseUri, projection, null, null, orderBy);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ // Swap the new cursor in. (The framework will take care of closing the
+ // old cursor once we return.)
+ mAdapter.swapCursor(data);
+
+ // The list should now be shown.
+ if (isResumed()) {
+ setListShown(true);
+ } else {
+ setListShownNoAnimation(true);
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ // This is called when the last Cursor provided to onLoadFinished()
+ // above is about to be closed. We need to make sure we are no
+ // longer using it.
+ mAdapter.swapCursor(null);
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ShareActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ShareActivity.java
new file mode 100644
index 000000000..29960748b
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ShareActivity.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * 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.sufficientlysecure.keychain.ui;
+
+import java.util.ArrayList;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.google.zxing.integration.android.IntentIntegrator;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+public class ShareActivity extends SherlockFragmentActivity {
+ public static final String ACTION_SHARE_KEYRING = Constants.INTENT_PREFIX + "SHARE_KEYRING";
+ public static final String ACTION_SHARE_KEYRING_WITH_QR_CODE = Constants.INTENT_PREFIX
+ + "SHARE_KEYRING_WITH_QR_CODE";
+
+ public static final String EXTRA_MASTER_KEY_ID = "masterKeyId";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ handleActions(getIntent());
+ }
+
+ protected void handleActions(Intent intent) {
+ String action = intent.getAction();
+ Bundle extras = intent.getExtras();
+
+ if (extras == null) {
+ extras = new Bundle();
+ }
+
+ long masterKeyId = extras.getLong(EXTRA_MASTER_KEY_ID);
+
+ // get public keyring as ascii armored string
+ ArrayList<String> keyringArmored = ProviderHelper.getPublicKeyRingsAsArmoredString(this,
+ new long[] { masterKeyId });
+
+ // close this activity
+ finish();
+
+ if (ACTION_SHARE_KEYRING.equals(action)) {
+ // let user choose application
+ Intent sendIntent = new Intent(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, keyringArmored.get(0));
+ sendIntent.setType("text/plain");
+ startActivity(Intent.createChooser(sendIntent,
+ getResources().getText(R.string.shareKeyringWith)));
+ } else if (ACTION_SHARE_KEYRING_WITH_QR_CODE.equals(action)) {
+ // use barcode scanner integration library
+ new IntentIntegrator(this).shareText(keyringArmored.get(0));
+ }
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ShareNfcBeamActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ShareNfcBeamActivity.java
new file mode 100644
index 000000000..c106614d3
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ShareNfcBeamActivity.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import net.nightwhistler.htmlspanner.HtmlSpanner;
+import net.nightwhistler.htmlspanner.JellyBeanSpanFixTextView;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.helper.OtherHelper;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuInflater;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.nfc.NdefMessage;
+import android.nfc.NdefRecord;
+import android.nfc.NfcAdapter;
+import android.nfc.NfcAdapter.CreateNdefMessageCallback;
+import android.nfc.NfcEvent;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.util.Log;
+import android.widget.Toast;
+import android.nfc.NfcAdapter.OnNdefPushCompleteCallback;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.Settings;
+import android.text.method.LinkMovementMethod;
+
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+public class ShareNfcBeamActivity extends SherlockFragmentActivity implements
+ CreateNdefMessageCallback, OnNdefPushCompleteCallback {
+ public static final String ACTION_SHARE_KEYRING_WITH_NFC = Constants.INTENT_PREFIX
+ + "SHARE_KEYRING_WITH_NFC";
+
+ public static final String EXTRA_MASTER_KEY_ID = "masterKeyId";
+
+ NfcAdapter mNfcAdapter;
+
+ byte[] mSharedKeyringBytes;
+
+ private static final int MESSAGE_SENT = 1;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
+ Toast.makeText(this,
+ getString(R.string.error) + ": " + getString(R.string.error_jellyBeanNeeded),
+ Toast.LENGTH_LONG).show();
+ finish();
+ } else {
+ // Check for available NFC Adapter
+ mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
+ if (mNfcAdapter == null) {
+ Toast.makeText(this,
+ getString(R.string.error) + ": " + getString(R.string.error_nfcNeeded),
+ Toast.LENGTH_LONG).show();
+ finish();
+ } else {
+ // handle actions after verifying that nfc works...
+ handleActions(getIntent());
+ }
+ }
+ }
+
+ protected void handleActions(Intent intent) {
+ String action = intent.getAction();
+ Bundle extras = intent.getExtras();
+
+ if (extras == null) {
+ extras = new Bundle();
+ }
+
+ if (ACTION_SHARE_KEYRING_WITH_NFC.equals(action)) {
+ long masterKeyId = extras.getLong(EXTRA_MASTER_KEY_ID);
+
+ // get public keyring as byte array
+ mSharedKeyringBytes = ProviderHelper.getPublicKeyRingsAsByteArray(this,
+ new long[] { masterKeyId });
+
+ // Register callback to set NDEF message
+ mNfcAdapter.setNdefPushMessageCallback(this, this);
+ // Register callback to listen for message-sent success
+ mNfcAdapter.setOnNdefPushCompleteCallback(this, this);
+ }
+ }
+
+ /**
+ * Parses the NDEF Message from the intent and prints to the TextView
+ */
+ void handleActionNdefDiscovered(Intent intent) {
+ Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
+ // only one message sent during the beam
+ NdefMessage msg = (NdefMessage) rawMsgs[0];
+ // record 0 contains the MIME type, record 1 is the AAR, if present
+ byte[] receivedKeyringBytes = msg.getRecords()[0].getPayload();
+
+ Intent importIntent = new Intent(this, ImportKeysActivity.class);
+ importIntent.setAction(ImportKeysActivity.ACTION_IMPORT);
+ importIntent.putExtra(ImportKeysActivity.EXTRA_KEYRING_BYTES, receivedKeyringBytes);
+
+ finish();
+
+ startActivity(importIntent);
+ }
+
+ private void buildView() {
+ // load html from html file from /res/raw
+ InputStream inputStreamText = getResources().openRawResource(R.raw.nfc_beam_share);
+
+ setContentView(R.layout.share_nfc_beam);
+
+ JellyBeanSpanFixTextView text = (JellyBeanSpanFixTextView) findViewById(R.id.nfc_beam_text);
+
+ // load html into textview
+ HtmlSpanner htmlSpanner = new HtmlSpanner();
+ htmlSpanner.setStripExtraWhiteSpace(true);
+ try {
+ text.setText(htmlSpanner.fromHtml(inputStreamText));
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "Error while reading raw resources as stream", e);
+ }
+
+ // make links work
+ text.setMovementMethod(LinkMovementMethod.getInstance());
+
+ // no flickering when clicking textview for Android < 4
+ text.setTextColor(getResources().getColor(android.R.color.black));
+
+ // set actionbar without home button if called from another app
+ OtherHelper.setActionBarBackButton(this);
+ }
+
+ /**
+ * Implementation for the CreateNdefMessageCallback interface
+ */
+ @Override
+ public NdefMessage createNdefMessage(NfcEvent event) {
+ /**
+ * When a device receives a push with an AAR in it, the application specified in the AAR is
+ * guaranteed to run. The AAR overrides the tag dispatch system. You can add it back in to
+ * guarantee that this activity starts when receiving a beamed message. For now, this code
+ * uses the tag dispatch system.
+ */
+ NdefMessage msg = new NdefMessage(NdefRecord.createMime(Constants.NFC_MIME,
+ mSharedKeyringBytes), NdefRecord.createApplicationRecord(Constants.PACKAGE_NAME));
+ return msg;
+ }
+
+ /**
+ * Implementation for the OnNdefPushCompleteCallback interface
+ */
+ @Override
+ public void onNdefPushComplete(NfcEvent arg0) {
+ // A handler is needed to send messages to the activity when this
+ // callback occurs, because it happens from a binder thread
+ mHandler.obtainMessage(MESSAGE_SENT).sendToTarget();
+ }
+
+ /** This handler receives a message from onNdefPushComplete */
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_SENT:
+ Toast.makeText(getApplicationContext(), R.string.nfcSuccessfull, Toast.LENGTH_LONG)
+ .show();
+ break;
+ }
+ }
+ };
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ // Check to see that the Activity started due to an Android Beam
+ if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) {
+ handleActionNdefDiscovered(getIntent());
+ } else {
+ // build view only when sending nfc, not when receiving, as it gets directly into Import
+ // activity on receiving
+ buildView();
+ }
+ }
+
+ @Override
+ public void onNewIntent(Intent intent) {
+ // onResume gets called after this to handle the intent
+ setIntent(intent);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getSupportMenuInflater();
+ inflater.inflate(R.menu.nfc_beam, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go to KeyListPublicActivity
+ Intent intent = new Intent(this, KeyListPublicActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+
+ case R.id.menu_settings:
+ Intent intentSettings = new Intent(Settings.ACTION_NFCSHARING_SETTINGS);
+ startActivity(intentSettings);
+ return true;
+
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SignKeyActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SignKeyActivity.java
new file mode 100644
index 000000000..21d55aca0
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SignKeyActivity.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright (C) 2011 Senecaso
+ *
+ * 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.sufficientlysecure.keychain.ui;
+
+import java.util.Iterator;
+
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPSignature;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.PgpMain;
+import org.sufficientlysecure.keychain.helper.Preferences;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.service.PassphraseCacheService;
+import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+
+import android.app.ProgressDialog;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Messenger;
+
+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 SherlockFragmentActivity {
+
+ public static final String EXTRA_KEY_ID = "keyId";
+
+ private long mPubKeyId = 0;
+ private long mMasterKeyId = 0;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // check we havent already signed it
+ setContentView(R.layout.sign_key_layout);
+
+ final ActionBar actionBar = getSupportActionBar();
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(false);
+ actionBar.setHomeButtonEnabled(false);
+
+ final Spinner keyServer = (Spinner) findViewById(R.id.keyServer);
+ ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
+ android.R.layout.simple_spinner_item, Preferences.getPreferences(this)
+ .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 (mPubKeyId != 0) {
+ initiateSigning();
+ }
+ }
+ });
+
+ mPubKeyId = getIntent().getLongExtra(EXTRA_KEY_ID, 0);
+ if (mPubKeyId == 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, SelectSecretKeyActivity.class);
+ startActivityForResult(intent, Id.request.secret_keys);
+ }
+ }
+
+ private void showPassphraseDialog(final long secretKeyId) {
+ // Message is received after passphrase is cached
+ Handler returnHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) {
+ startSigning();
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(returnHandler);
+
+ try {
+ PassphraseDialogFragment passphraseDialog = PassphraseDialogFragment.newInstance(this,
+ messenger, secretKeyId);
+
+ passphraseDialog.show(getSupportFragmentManager(), "passphraseDialog");
+ } catch (PgpMain.PgpGeneralException e) {
+ Log.d(Constants.TAG, "No passphrase for this secret key, encrypt directly!");
+ // send message to handler to start encryption directly
+ returnHandler.sendEmptyMessage(PassphraseDialogFragment.MESSAGE_OKAY);
+ }
+ }
+
+ /**
+ * handles the UI bits of the signing process on the UI thread
+ */
+ private void initiateSigning() {
+ PGPPublicKeyRing pubring = ProviderHelper.getPGPPublicKeyRingByMasterKeyId(this, mPubKeyId);
+ 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(mPubKeyId).getSignatures();
+ while (itr.hasNext()) {
+ PGPSignature sig = itr.next();
+ if (sig.getKeyID() == mMasterKeyId) {
+ alreadySigned = true;
+ break;
+ }
+ }
+
+ if (!alreadySigned) {
+ /*
+ * get the user's passphrase for this key (if required)
+ */
+ String passphrase = PassphraseCacheService.getCachedPassphrase(this, mMasterKeyId);
+ if (passphrase == null) {
+ showPassphraseDialog(mMasterKeyId);
+ return; // bail out; need to wait until the user has entered the passphrase
+ // before trying again
+ } else {
+ startSigning();
+ }
+ } else {
+ Toast.makeText(this, "Key has already been signed", Toast.LENGTH_SHORT).show();
+
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ }
+ }
+
+ /**
+ * kicks off the actual signing process on a background thread
+ */
+ private void startSigning() {
+ // Send all information needed to service to sign key in other thread
+ Intent intent = new Intent(this, KeychainIntentService.class);
+
+ intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_SIGN_KEYRING);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ data.putLong(KeychainIntentService.SIGN_KEY_MASTER_KEY_ID, mMasterKeyId);
+ data.putLong(KeychainIntentService.SIGN_KEY_PUB_KEY_ID, mPubKeyId);
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after signing is done in ApgService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this, R.string.progress_signing,
+ ProgressDialog.STYLE_SPINNER) {
+ public void handleMessage(Message message) {
+ // handle messages by standard ApgHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+
+ Toast.makeText(SignKeyActivity.this, R.string.keySignSuccess,
+ Toast.LENGTH_SHORT).show();
+
+ // check if we need to send the key to the server or not
+ CheckBox sendKey = (CheckBox) findViewById(R.id.sendKey);
+ if (sendKey.isChecked()) {
+ /*
+ * upload the newly signed key to the key server
+ */
+ uploadKey();
+ } else {
+ finish();
+ }
+ }
+ };
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // show progress dialog
+ saveHandler.showProgressDialog(this);
+
+ // start service with intent
+ startService(intent);
+ }
+
+ private void uploadKey() {
+ // Send all information needed to service to upload key in other thread
+ Intent intent = new Intent(this, KeychainIntentService.class);
+
+ intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_UPLOAD_KEYRING);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ data.putLong(KeychainIntentService.UPLOAD_KEY_KEYRING_ROW_ID, mPubKeyId);
+
+ Spinner keyServer = (Spinner) findViewById(R.id.keyServer);
+ String server = (String) keyServer.getSelectedItem();
+ data.putString(KeychainIntentService.UPLOAD_KEY_SERVER, server);
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after uploading is done in ApgService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this, R.string.progress_exporting,
+ ProgressDialog.STYLE_HORIZONTAL) {
+ public void handleMessage(Message message) {
+ // handle messages by standard ApgHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+
+ Toast.makeText(SignKeyActivity.this, R.string.keySendSuccess,
+ Toast.LENGTH_SHORT).show();
+
+ finish();
+ }
+ };
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // show progress dialog
+ saveHandler.showProgressDialog(this);
+
+ // start service with intent
+ startService(intent);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.request.secret_keys: {
+ if (resultCode == RESULT_OK) {
+ mMasterKeyId = data.getLongExtra(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);
+ }
+ }
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/DeleteFileDialogFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/DeleteFileDialogFragment.java
new file mode 100644
index 000000000..defc3bc6c
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/DeleteFileDialogFragment.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * 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.sufficientlysecure.keychain.ui.dialog;
+
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.R;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.Messenger;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.FragmentActivity;
+import android.widget.Toast;
+
+public class DeleteFileDialogFragment extends DialogFragment {
+ private static final String ARG_DELETE_FILE = "delete_file";
+
+ /**
+ * Creates new instance of this delete file dialog fragment
+ */
+ public static DeleteFileDialogFragment newInstance(String deleteFile) {
+ DeleteFileDialogFragment frag = new DeleteFileDialogFragment();
+ Bundle args = new Bundle();
+
+ args.putString(ARG_DELETE_FILE, deleteFile);
+
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ /**
+ * Creates dialog
+ */
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final FragmentActivity activity = getActivity();
+
+ final String deleteFile = getArguments().getString(ARG_DELETE_FILE);
+
+ AlertDialog.Builder alert = new AlertDialog.Builder(activity);
+
+ alert.setIcon(android.R.drawable.ic_dialog_alert);
+ alert.setTitle(R.string.warning);
+ alert.setMessage(this.getString(R.string.fileDeleteConfirmation, deleteFile));
+
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+
+ // Send all information needed to service to edit key in other thread
+ Intent intent = new Intent(activity, KeychainIntentService.class);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_DELETE_FILE_SECURELY);
+ data.putString(KeychainIntentService.DELETE_FILE, deleteFile);
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ ProgressDialogFragment deletingDialog = ProgressDialogFragment.newInstance(
+ R.string.progress_deletingSecurely, ProgressDialog.STYLE_HORIZONTAL);
+
+ // Message is received after deleting is done in ApgService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(activity, deletingDialog) {
+ public void handleMessage(Message message) {
+ // handle messages by standard ApgHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ Toast.makeText(activity, R.string.fileDeleteSuccessful,
+ Toast.LENGTH_SHORT).show();
+ }
+ };
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // show progress dialog
+ deletingDialog.show(activity.getSupportFragmentManager(), "deletingDialog");
+
+ // start service with intent
+ activity.startService(intent);
+ }
+ });
+ alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+ }
+ });
+ alert.setCancelable(true);
+
+ return alert.create();
+ }
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java
new file mode 100644
index 000000000..e61003a9b
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * 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.sufficientlysecure.keychain.ui.dialog;
+
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.FragmentActivity;
+
+public class DeleteKeyDialogFragment extends DialogFragment {
+ private static final String ARG_MESSENGER = "messenger";
+ private static final String ARG_DELETE_KEY_RING_ROW_ID = "delete_file";
+ private static final String ARG_KEY_TYPE = "key_type";
+
+ public static final int MESSAGE_OKAY = 1;
+
+ private Messenger mMessenger;
+
+ /**
+ * Creates new instance of this delete file dialog fragment
+ */
+ public static DeleteKeyDialogFragment newInstance(Messenger messenger, long deleteKeyRingRowId,
+ int keyType) {
+ DeleteKeyDialogFragment frag = new DeleteKeyDialogFragment();
+ Bundle args = new Bundle();
+
+ args.putParcelable(ARG_MESSENGER, messenger);
+ args.putLong(ARG_DELETE_KEY_RING_ROW_ID, deleteKeyRingRowId);
+ args.putInt(ARG_KEY_TYPE, keyType);
+
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ /**
+ * Creates dialog
+ */
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final FragmentActivity activity = getActivity();
+ mMessenger = getArguments().getParcelable(ARG_MESSENGER);
+
+ final long deleteKeyRingRowId = getArguments().getLong(ARG_DELETE_KEY_RING_ROW_ID);
+ final int keyType = getArguments().getInt(ARG_KEY_TYPE);
+
+ // TODO: better way to do this?
+ String userId = activity.getString(R.string.unknownUserId);
+
+ if (keyType == Id.type.public_key) {
+ PGPPublicKeyRing keyRing = ProviderHelper.getPGPPublicKeyRingByRowId(activity,
+ deleteKeyRingRowId);
+ userId = PgpHelper.getMainUserIdSafe(activity, PgpHelper.getMasterKey(keyRing));
+ } else {
+ PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingByRowId(activity,
+ deleteKeyRingRowId);
+ userId = PgpHelper.getMainUserIdSafe(activity, PgpHelper.getMasterKey(keyRing));
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ builder.setTitle(R.string.warning);
+ builder.setMessage(getString(
+ keyType == 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() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ if (keyType == Id.type.public_key) {
+ ProviderHelper.deletePublicKeyRing(activity, deleteKeyRingRowId);
+ } else {
+ ProviderHelper.deleteSecretKeyRing(activity, deleteKeyRingRowId);
+ }
+
+ dismiss();
+
+ sendMessageToHandler(MESSAGE_OKAY);
+ }
+ });
+ builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+ }
+ });
+ return builder.create();
+ }
+
+ /**
+ * Send message back to handler which is initialized in a activity
+ *
+ * @param what
+ * Message integer you want to send
+ */
+ private void sendMessageToHandler(Integer what) {
+ Message msg = Message.obtain();
+ msg.what = what;
+
+ try {
+ mMessenger.send(msg);
+ } catch (RemoteException e) {
+ Log.w(Constants.TAG, "Exception sending message, Is handler present?", e);
+ } catch (NullPointerException e) {
+ Log.w(Constants.TAG, "Messenger is null!", e);
+ }
+ }
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java
new file mode 100644
index 000000000..2f9cdb3b1
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * 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.sufficientlysecure.keychain.ui.dialog;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.helper.FileHelper;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.support.v4.app.DialogFragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ImageButton;
+
+public class FileDialogFragment extends DialogFragment {
+ private static final String ARG_MESSENGER = "messenger";
+ private static final String ARG_TITLE = "title";
+ private static final String ARG_MESSAGE = "message";
+ private static final String ARG_DEFAULT_FILE = "default_file";
+ private static final String ARG_CHECKBOX_TEXT = "checkbox_text";
+ private static final String ARG_REQUEST_CODE = "request_code";
+
+ public static final int MESSAGE_OKAY = 1;
+
+ public static final String MESSAGE_DATA_FILENAME = "filename";
+ public static final String MESSAGE_DATA_CHECKED = "checked";
+
+ private Messenger mMessenger;
+
+ /**
+ * Creates new instance of this file dialog fragment
+ */
+ public static FileDialogFragment newInstance(Messenger messenger, String title, String message,
+ String defaultFile, String checkboxText, int requestCode) {
+ FileDialogFragment frag = new FileDialogFragment();
+ Bundle args = new Bundle();
+ args.putParcelable(ARG_MESSENGER, messenger);
+
+ args.putString(ARG_TITLE, title);
+ args.putString(ARG_MESSAGE, message);
+ args.putString(ARG_DEFAULT_FILE, defaultFile);
+ args.putString(ARG_CHECKBOX_TEXT, checkboxText);
+ args.putInt(ARG_REQUEST_CODE, requestCode);
+
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ /**
+ * Creates dialog
+ */
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Activity activity = getActivity();
+
+ mMessenger = getArguments().getParcelable(ARG_MESSENGER);
+
+ String title = getArguments().getString(ARG_TITLE);
+ String message = getArguments().getString(ARG_MESSAGE);
+ String defaultFile = getArguments().getString(ARG_DEFAULT_FILE);
+ String checkboxText = getArguments().getString(ARG_CHECKBOX_TEXT);
+ final int requestCode = getArguments().getInt(ARG_REQUEST_CODE);
+
+ final EditText mFilename;
+ final ImageButton mBrowse;
+ final CheckBox mCheckBox;
+
+ LayoutInflater inflater = (LayoutInflater) activity
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ AlertDialog.Builder alert = new AlertDialog.Builder(activity);
+
+ alert.setTitle(title);
+ alert.setMessage(message);
+
+ View view = inflater.inflate(R.layout.file_dialog, null);
+
+ mFilename = (EditText) view.findViewById(R.id.input);
+ mFilename.setText(defaultFile);
+ mBrowse = (ImageButton) view.findViewById(R.id.btn_browse);
+ mBrowse.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ // only .asc or .gpg files
+ FileHelper.openFile(activity, mFilename.getText().toString(), "text/plain",
+ requestCode);
+ }
+ });
+
+ mCheckBox = (CheckBox) view.findViewById(R.id.checkbox);
+ if (checkboxText == null) {
+ mCheckBox.setEnabled(false);
+ mCheckBox.setVisibility(View.GONE);
+ } else {
+ mCheckBox.setEnabled(true);
+ mCheckBox.setVisibility(View.VISIBLE);
+ mCheckBox.setText(checkboxText);
+ }
+
+ alert.setView(view);
+
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+
+ boolean checked = false;
+ if (mCheckBox.isEnabled()) {
+ checked = mCheckBox.isChecked();
+ }
+
+ // return resulting data back to activity
+ Bundle data = new Bundle();
+ data.putString(MESSAGE_DATA_FILENAME, mFilename.getText().toString());
+ data.putBoolean(MESSAGE_DATA_CHECKED, checked);
+
+ sendMessageToHandler(MESSAGE_OKAY, data);
+ }
+ });
+
+ alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+ }
+ });
+ return alert.create();
+ }
+
+ /**
+ * Updates filename in dialog, normally called in onActivityResult in activity using the
+ * FileDialog
+ *
+ * @param messageId
+ * @param progress
+ * @param max
+ */
+ public void setFilename(String filename) {
+ AlertDialog dialog = (AlertDialog) getDialog();
+ EditText filenameEditText = (EditText) dialog.findViewById(R.id.input);
+
+ if (filenameEditText != null) {
+ filenameEditText.setText(filename);
+ }
+ }
+
+ /**
+ * Send message back to handler which is initialized in a activity
+ *
+ * @param what
+ * Message integer you want to send
+ */
+ private void sendMessageToHandler(Integer what, Bundle data) {
+ Message msg = Message.obtain();
+ msg.what = what;
+ if (data != null) {
+ msg.setData(data);
+ }
+
+ try {
+ mMessenger.send(msg);
+ } catch (RemoteException e) {
+ Log.w(Constants.TAG, "Exception sending message, Is handler present?", e);
+ } catch (NullPointerException e) {
+ Log.w(Constants.TAG, "Messenger is null!", e);
+ }
+ }
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/LookupUnknownKeyDialogFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/LookupUnknownKeyDialogFragment.java
new file mode 100644
index 000000000..17f900552
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/LookupUnknownKeyDialogFragment.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * 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.sufficientlysecure.keychain.ui.dialog;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.ui.KeyServerQueryActivity;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.support.v4.app.DialogFragment;
+
+
+public class LookupUnknownKeyDialogFragment extends DialogFragment {
+ private static final String ARG_MESSENGER = "messenger";
+ private static final String ARG_UNKNOWN_KEY_ID = "unknown_key_id";
+
+ public static final int MESSAGE_OKAY = 1;
+ public static final int MESSAGE_CANCEL = 2;
+
+ private Messenger mMessenger;
+
+ /**
+ * Creates new instance of this dialog fragment
+ *
+ * @param messenger
+ * @param unknownKeyId
+ * @return
+ */
+ public static LookupUnknownKeyDialogFragment newInstance(Messenger messenger, long unknownKeyId) {
+ LookupUnknownKeyDialogFragment frag = new LookupUnknownKeyDialogFragment();
+ Bundle args = new Bundle();
+ args.putLong(ARG_UNKNOWN_KEY_ID, unknownKeyId);
+ args.putParcelable(ARG_MESSENGER, messenger);
+
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ /**
+ * Creates dialog
+ */
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Activity activity = getActivity();
+
+ final long unknownKeyId = getArguments().getLong(ARG_UNKNOWN_KEY_ID);
+ mMessenger = getArguments().getParcelable(ARG_MESSENGER);
+
+ AlertDialog.Builder alert = new AlertDialog.Builder(activity);
+
+ alert.setIcon(android.R.drawable.ic_dialog_alert);
+ alert.setTitle(R.string.title_unknownSignatureKey);
+ alert.setMessage(getString(R.string.lookupUnknownKey,
+ PgpHelper.getSmallFingerPrint(unknownKeyId)));
+
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+
+ sendMessageToHandler(MESSAGE_OKAY);
+
+ Intent intent = new Intent(activity, KeyServerQueryActivity.class);
+ intent.setAction(KeyServerQueryActivity.ACTION_LOOK_UP_KEY_ID);
+ intent.putExtra(KeyServerQueryActivity.EXTRA_KEY_ID, unknownKeyId);
+ startActivityForResult(intent, Id.request.look_up_key_id);
+ }
+ });
+ alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+
+ sendMessageToHandler(MESSAGE_CANCEL);
+ }
+ });
+ alert.setCancelable(true);
+ alert.setOnCancelListener(new OnCancelListener() {
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ sendMessageToHandler(MESSAGE_CANCEL);
+ }
+ });
+
+ return alert.create();
+ }
+
+ /**
+ * Send message back to handler which is initialized in a activity
+ *
+ * @param what
+ * Message integer you want to send
+ */
+ private void sendMessageToHandler(Integer what) {
+ Message msg = Message.obtain();
+ msg.what = what;
+
+ try {
+ mMessenger.send(msg);
+ } catch (RemoteException e) {
+ Log.w(Constants.TAG, "Exception sending message, Is handler present?", e);
+ } catch (NullPointerException e) {
+ Log.w(Constants.TAG, "Messenger is null!", e);
+ }
+ }
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java
new file mode 100644
index 000000000..504de9d69
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * 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.sufficientlysecure.keychain.ui.dialog;
+
+import org.spongycastle.openpgp.PGPException;
+import org.spongycastle.openpgp.PGPPrivateKey;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.operator.PBESecretKeyDecryptor;
+import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.helper.PgpMain;
+import org.sufficientlysecure.keychain.helper.PgpMain.PgpGeneralException;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.service.PassphraseCacheService;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.support.v4.app.DialogFragment;
+
+
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager.LayoutParams;
+import android.view.inputmethod.EditorInfo;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+import android.widget.Toast;
+
+public class PassphraseDialogFragment extends DialogFragment implements OnEditorActionListener {
+ private static final String ARG_MESSENGER = "messenger";
+ private static final String ARG_SECRET_KEY_ID = "secret_key_id";
+
+ public static final int MESSAGE_OKAY = 1;
+
+ private Messenger mMessenger;
+ private EditText mPassphraseEditText;
+
+ /**
+ * Creates new instance of this dialog fragment
+ *
+ * @param secretKeyId
+ * secret key id you want to use
+ * @param messenger
+ * to communicate back after caching the passphrase
+ * @return
+ * @throws PgpGeneralException
+ */
+ public static PassphraseDialogFragment newInstance(Context context, Messenger messenger,
+ long secretKeyId) throws PgpGeneralException {
+ // check if secret key has a passphrase
+ if (!(secretKeyId == Id.key.symmetric || secretKeyId == Id.key.none)) {
+ if (!hasPassphrase(context, secretKeyId)) {
+ throw new PgpMain.PgpGeneralException("No passphrase! No passphrase dialog needed!");
+ }
+ }
+
+ PassphraseDialogFragment frag = new PassphraseDialogFragment();
+ Bundle args = new Bundle();
+ args.putLong(ARG_SECRET_KEY_ID, secretKeyId);
+ args.putParcelable(ARG_MESSENGER, messenger);
+
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ /**
+ * Checks if key has a passphrase
+ *
+ * @param secretKeyId
+ * @return true if it has a passphrase
+ */
+ private static boolean hasPassphrase(Context context, long secretKeyId) {
+ // check if the key has no passphrase
+ try {
+ PGPSecretKey secretKey = PgpHelper.getMasterKey(ProviderHelper
+ .getPGPSecretKeyRingByKeyId(context, secretKeyId));
+ // PGPSecretKey secretKey =
+ // PGPHelper.getMasterKey(PGPMain.getSecretKeyRing(secretKeyId));
+
+ Log.d(Constants.TAG, "Check if key has no passphrase...");
+ PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
+ "SC").build("".toCharArray());
+ PGPPrivateKey testKey = secretKey.extractPrivateKey(keyDecryptor);
+ if (testKey != null) {
+ Log.d(Constants.TAG, "Key has no passphrase! Caches empty passphrase!");
+
+ // cache empty passphrase
+ PassphraseCacheService.addCachedPassphrase(context, secretKey.getKeyID(), "");
+
+ return false;
+ }
+ } catch (PGPException e) {
+ // silently catch
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ /**
+ * Creates dialog
+ */
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Activity activity = getActivity();
+
+ long secretKeyId = getArguments().getLong(ARG_SECRET_KEY_ID);
+ mMessenger = getArguments().getParcelable(ARG_MESSENGER);
+
+ AlertDialog.Builder alert = new AlertDialog.Builder(activity);
+
+ alert.setTitle(R.string.title_authentication);
+
+ final PGPSecretKey secretKey;
+
+ if (secretKeyId == Id.key.symmetric || secretKeyId == Id.key.none) {
+ secretKey = null;
+ alert.setMessage(R.string.passPhraseForSymmetricEncryption);
+ } else {
+ // TODO: by master key id???
+ secretKey = PgpHelper.getMasterKey(ProviderHelper.getPGPSecretKeyRingByMasterKeyId(
+ activity, secretKeyId));
+ // secretKey = PGPHelper.getMasterKey(PGPMain.getSecretKeyRing(secretKeyId));
+
+ if (secretKey == null) {
+ alert.setTitle(R.string.title_keyNotFound);
+ alert.setMessage(getString(R.string.keyNotFound, secretKeyId));
+ alert.setPositiveButton(android.R.string.ok, new OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ dismiss();
+ }
+ });
+ alert.setCancelable(false);
+ return alert.create();
+ }
+ String userId = PgpHelper.getMainUserIdSafe(activity, secretKey);
+
+ Log.d(Constants.TAG, "User id: '" + userId + "'");
+ alert.setMessage(getString(R.string.passPhraseFor, userId));
+ }
+
+ LayoutInflater inflater = activity.getLayoutInflater();
+ View view = inflater.inflate(R.layout.passphrase, null);
+ alert.setView(view);
+
+ mPassphraseEditText = (EditText) view.findViewById(R.id.passphrase_passphrase);
+
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+
+ String passPhrase = mPassphraseEditText.getText().toString();
+ long keyId;
+ if (secretKey != null) {
+ try {
+ PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder()
+ .setProvider(PgpMain.BOUNCY_CASTLE_PROVIDER_NAME).build(
+ passPhrase.toCharArray());
+ PGPPrivateKey testKey = secretKey.extractPrivateKey(keyDecryptor);
+ if (testKey == null) {
+ Toast.makeText(activity, R.string.error_couldNotExtractPrivateKey,
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ } catch (PGPException e) {
+ Toast.makeText(activity, R.string.wrongPassPhrase, Toast.LENGTH_SHORT)
+ .show();
+ return;
+ }
+ keyId = secretKey.getKeyID();
+ } else {
+ keyId = Id.key.symmetric;
+ }
+
+ // cache the new passphrase
+ Log.d(Constants.TAG, "Everything okay! Caching entered passphrase");
+ PassphraseCacheService.addCachedPassphrase(activity, keyId, passPhrase);
+
+ sendMessageToHandler(MESSAGE_OKAY);
+ }
+ });
+
+ alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+ }
+ });
+
+ return alert.create();
+ }
+
+ @Override
+ public void onActivityCreated(Bundle arg0) {
+ super.onActivityCreated(arg0);
+
+ // request focus and open soft keyboard
+ mPassphraseEditText.requestFocus();
+ getDialog().getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_VISIBLE);
+
+ mPassphraseEditText.setOnEditorActionListener(this);
+ }
+
+ /**
+ * Associate the "done" button on the soft keyboard with the okay button in the view
+ */
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (EditorInfo.IME_ACTION_DONE == actionId) {
+ AlertDialog dialog = ((AlertDialog) getDialog());
+ Button bt = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
+
+ bt.performClick();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Send message back to handler which is initialized in a activity
+ *
+ * @param what
+ * Message integer you want to send
+ */
+ private void sendMessageToHandler(Integer what) {
+ Message msg = Message.obtain();
+ msg.what = what;
+
+ try {
+ mMessenger.send(msg);
+ } catch (RemoteException e) {
+ Log.w(Constants.TAG, "Exception sending message, Is handler present?", e);
+ } catch (NullPointerException e) {
+ Log.w(Constants.TAG, "Messenger is null!", e);
+ }
+ }
+
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/ProgressDialogFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/ProgressDialogFragment.java
new file mode 100644
index 000000000..69b31419e
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/ProgressDialogFragment.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * 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.sufficientlysecure.keychain.ui.dialog;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnKeyListener;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.view.KeyEvent;
+
+public class ProgressDialogFragment extends DialogFragment {
+ private static final String ARG_MESSAGE_ID = "message_id";
+ private static final String ARG_STYLE = "style";
+
+ /**
+ * Creates new instance of this fragment
+ *
+ * @param id
+ * @return
+ */
+ public static ProgressDialogFragment newInstance(int messageId, int style) {
+ ProgressDialogFragment frag = new ProgressDialogFragment();
+ Bundle args = new Bundle();
+ args.putInt(ARG_MESSAGE_ID, messageId);
+ args.putInt(ARG_STYLE, style);
+
+ frag.setArguments(args);
+ return frag;
+ }
+
+ /**
+ * Updates progress of dialog
+ *
+ * @param messageId
+ * @param progress
+ * @param max
+ */
+ public void setProgress(int messageId, int progress, int max) {
+ setProgress(getString(messageId), progress, max);
+ }
+
+ /**
+ * Updates progress of dialog
+ *
+ * @param messageId
+ * @param progress
+ * @param max
+ */
+ public void setProgress(int progress, int max) {
+ ProgressDialog dialog = (ProgressDialog) getDialog();
+
+ dialog.setProgress(progress);
+ dialog.setMax(max);
+ }
+
+ /**
+ * Updates progress of dialog
+ *
+ * @param messageId
+ * @param progress
+ * @param max
+ */
+ public void setProgress(String message, int progress, int max) {
+ ProgressDialog dialog = (ProgressDialog) getDialog();
+
+ dialog.setMessage(message);
+ dialog.setProgress(progress);
+ dialog.setMax(max);
+ }
+
+ /**
+ * Creates dialog
+ */
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ Activity activity = getActivity();
+
+ ProgressDialog dialog = new ProgressDialog(activity);
+ dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ dialog.setCancelable(false);
+ dialog.setCanceledOnTouchOutside(false);
+
+ int messageId = getArguments().getInt(ARG_MESSAGE_ID);
+ int style = getArguments().getInt(ARG_STYLE);
+
+ dialog.setMessage(getString(messageId));
+ dialog.setProgressStyle(style);
+
+ // Disable the back button
+ OnKeyListener keyListener = new OnKeyListener() {
+
+ @Override
+ public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
+
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ return true;
+ }
+ return false;
+ }
+
+ };
+ dialog.setOnKeyListener(keyListener);
+
+ return dialog;
+ }
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java
new file mode 100644
index 000000000..f001e7a7d
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * 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.sufficientlysecure.keychain.ui.dialog;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.support.v4.app.DialogFragment;
+
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager.LayoutParams;
+import android.view.inputmethod.EditorInfo;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.TextView.OnEditorActionListener;
+
+public class SetPassphraseDialogFragment extends DialogFragment implements OnEditorActionListener {
+ private static final String ARG_MESSENGER = "messenger";
+ private static final String ARG_TITLE = "title";
+
+ public static final int MESSAGE_OKAY = 1;
+
+ public static final String MESSAGE_NEW_PASSPHRASE = "new_passphrase";
+
+ private Messenger mMessenger;
+ private EditText mPassphraseEditText;
+ private EditText mPassphraseAgainEditText;
+
+ /**
+ * Creates new instance of this dialog fragment
+ *
+ * @param title
+ * title of dialog
+ * @param messenger
+ * to communicate back after setting the passphrase
+ * @return
+ */
+ public static SetPassphraseDialogFragment newInstance(Messenger messenger, int title) {
+ SetPassphraseDialogFragment frag = new SetPassphraseDialogFragment();
+ Bundle args = new Bundle();
+ args.putInt(ARG_TITLE, title);
+ args.putParcelable(ARG_MESSENGER, messenger);
+
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ /**
+ * Creates dialog
+ */
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Activity activity = getActivity();
+
+ int title = getArguments().getInt(ARG_TITLE);
+ mMessenger = getArguments().getParcelable(ARG_MESSENGER);
+
+ AlertDialog.Builder alert = new AlertDialog.Builder(activity);
+
+ alert.setTitle(title);
+ alert.setMessage(R.string.enterPassPhraseTwice);
+
+ LayoutInflater inflater = activity.getLayoutInflater();
+ View view = inflater.inflate(R.layout.passphrase_repeat, null);
+ alert.setView(view);
+
+ mPassphraseEditText = (EditText) view.findViewById(R.id.passphrase_passphrase);
+ mPassphraseAgainEditText = (EditText) view.findViewById(R.id.passphrase_passphrase_again);
+
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+
+ String passPhrase1 = mPassphraseEditText.getText().toString();
+ String passPhrase2 = mPassphraseAgainEditText.getText().toString();
+ if (!passPhrase1.equals(passPhrase2)) {
+ Toast.makeText(
+ activity,
+ getString(R.string.errorMessage,
+ getString(R.string.passPhrasesDoNotMatch)), Toast.LENGTH_SHORT)
+ .show();
+ return;
+ }
+
+ if (passPhrase1.equals("")) {
+ Toast.makeText(
+ activity,
+ getString(R.string.errorMessage,
+ getString(R.string.passPhraseMustNotBeEmpty)),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // return resulting data back to activity
+ Bundle data = new Bundle();
+ data.putString(MESSAGE_NEW_PASSPHRASE, passPhrase1);
+
+ sendMessageToHandler(MESSAGE_OKAY, data);
+ }
+ });
+
+ alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+ }
+ });
+
+ return alert.create();
+ }
+
+ @Override
+ public void onActivityCreated(Bundle arg0) {
+ super.onActivityCreated(arg0);
+
+ // request focus and open soft keyboard
+ mPassphraseEditText.requestFocus();
+ getDialog().getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_VISIBLE);
+
+ mPassphraseAgainEditText.setOnEditorActionListener(this);
+ }
+
+ /**
+ * Associate the "done" button on the soft keyboard with the okay button in the view
+ */
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (EditorInfo.IME_ACTION_DONE == actionId) {
+ AlertDialog dialog = ((AlertDialog) getDialog());
+ Button bt = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
+
+ bt.performClick();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Send message back to handler which is initialized in a activity
+ *
+ * @param what
+ * Message integer you want to send
+ */
+ private void sendMessageToHandler(Integer what, Bundle data) {
+ Message msg = Message.obtain();
+ msg.what = what;
+ if (data != null) {
+ msg.setData(data);
+ }
+
+ try {
+ mMessenger.send(msg);
+ } catch (RemoteException e) {
+ Log.w(Constants.TAG, "Exception sending message, Is handler present?", e);
+ } catch (NullPointerException e) {
+ Log.w(Constants.TAG, "Messenger is null!", e);
+ }
+ }
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/DashboardLayout.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/DashboardLayout.java
new file mode 100644
index 000000000..158a271bc
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/DashboardLayout.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2011 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.sufficientlysecure.keychain.ui.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Custom layout that arranges children in a grid-like manner, optimizing for even horizontal and
+ * vertical whitespace.
+ */
+public class DashboardLayout extends ViewGroup {
+ private static final int UNEVEN_GRID_PENALTY_MULTIPLIER = 10;
+
+ private int mMaxChildWidth = 0;
+ private int mMaxChildHeight = 0;
+
+ public DashboardLayout(Context context) {
+ super(context, null);
+ }
+
+ public DashboardLayout(Context context, AttributeSet attrs) {
+ super(context, attrs, 0);
+ }
+
+ public DashboardLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ mMaxChildWidth = 0;
+ mMaxChildHeight = 0;
+
+ // Measure once to find the maximum child size.
+
+ int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+ MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.AT_MOST);
+ int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+ MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.AT_MOST);
+
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+
+ mMaxChildWidth = Math.max(mMaxChildWidth, child.getMeasuredWidth());
+ mMaxChildHeight = Math.max(mMaxChildHeight, child.getMeasuredHeight());
+ }
+
+ // Measure again for each child to be exactly the same size.
+
+ childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxChildWidth, MeasureSpec.EXACTLY);
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxChildHeight, MeasureSpec.EXACTLY);
+
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+
+ setMeasuredDimension(resolveSize(mMaxChildWidth, widthMeasureSpec),
+ resolveSize(mMaxChildHeight, heightMeasureSpec));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ int width = r - l;
+ int height = b - t;
+
+ final int count = getChildCount();
+
+ // Calculate the number of visible children.
+ int visibleCount = 0;
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+ ++visibleCount;
+ }
+
+ if (visibleCount == 0) {
+ return;
+ }
+
+ // Calculate what number of rows and columns will optimize for even horizontal and
+ // vertical whitespace between items. Start with a 1 x N grid, then try 2 x N, and so on.
+ int bestSpaceDifference = Integer.MAX_VALUE;
+ int spaceDifference;
+
+ // Horizontal and vertical space between items
+ int hSpace = 0;
+ int vSpace = 0;
+
+ int cols = 1;
+ int rows;
+
+ while (true) {
+ rows = (visibleCount - 1) / cols + 1;
+
+ hSpace = ((width - mMaxChildWidth * cols) / (cols + 1));
+ vSpace = ((height - mMaxChildHeight * rows) / (rows + 1));
+
+ spaceDifference = Math.abs(vSpace - hSpace);
+ if (rows * cols != visibleCount) {
+ spaceDifference *= UNEVEN_GRID_PENALTY_MULTIPLIER;
+ } else if (rows * mMaxChildHeight > height || cols * mMaxChildWidth > width) {
+ spaceDifference *= UNEVEN_GRID_PENALTY_MULTIPLIER;
+ }
+
+ if (spaceDifference < bestSpaceDifference) {
+ // Found a better whitespace squareness/ratio
+ bestSpaceDifference = spaceDifference;
+
+ // If we found a better whitespace squareness and there's only 1 row, this is
+ // the best we can do.
+ if (rows == 1) {
+ break;
+ }
+ } else {
+ // This is a worse whitespace ratio, use the previous value of cols and exit.
+ --cols;
+ rows = (visibleCount - 1) / cols + 1;
+ hSpace = ((width - mMaxChildWidth * cols) / (cols + 1));
+ vSpace = ((height - mMaxChildHeight * rows) / (rows + 1));
+ break;
+ }
+
+ ++cols;
+ }
+
+ // Lay out children based on calculated best-fit number of rows and cols.
+
+ // If we chose a layout that has negative horizontal or vertical space, force it to zero.
+ hSpace = Math.max(0, hSpace);
+ vSpace = Math.max(0, vSpace);
+
+ // Re-use width/height variables to be child width/height.
+ width = (width - hSpace * (cols + 1)) / cols;
+ height = (height - vSpace * (rows + 1)) / rows;
+
+ int left, top;
+ int col, row;
+ int visibleIndex = 0;
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+
+ row = visibleIndex / cols;
+ col = visibleIndex % cols;
+
+ left = hSpace * (col + 1) + width * col;
+ top = vSpace * (row + 1) + height * row;
+
+ child.layout(left, top, (hSpace == 0 && col == cols - 1) ? r : (left + width),
+ (vSpace == 0 && row == rows - 1) ? b : (top + height));
+ ++visibleIndex;
+ }
+ }
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/Editor.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/Editor.java
new file mode 100644
index 000000000..1cf510d3a
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/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.sufficientlysecure.keychain.ui.widget;
+
+public interface Editor {
+ public interface EditorListener {
+ public void onDeleted(Editor editor);
+ }
+
+ public void setEditorListener(EditorListener listener);
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/ExpandableListFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/ExpandableListFragment.java
new file mode 100644
index 000000000..54022342f
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/ExpandableListFragment.java
@@ -0,0 +1,530 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui.widget;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v4.app.Fragment;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnCreateContextMenuListener;
+import android.view.ViewGroup;
+import android.view.animation.AnimationUtils;
+import android.widget.AdapterView;
+import android.widget.ExpandableListAdapter;
+import android.widget.ExpandableListView;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.ListAdapter;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+/**
+ * @author Khoa Tran
+ *
+ * @see android.support.v4.app.ListFragment
+ * @see android.app.ExpandableListActivity
+ *
+ * ExpandableListFragment for Android < 3.0
+ *
+ * from
+ * http://stackoverflow.com/questions/6051050/expandablelistfragment-with-loadermanager-for-
+ * compatibility-package
+ *
+ */
+public class ExpandableListFragment extends Fragment implements OnCreateContextMenuListener,
+ ExpandableListView.OnChildClickListener, ExpandableListView.OnGroupCollapseListener,
+ ExpandableListView.OnGroupExpandListener {
+
+ static final int INTERNAL_EMPTY_ID = 0x00ff0001;
+ static final int INTERNAL_PROGRESS_CONTAINER_ID = 0x00ff0002;
+ static final int INTERNAL_LIST_CONTAINER_ID = 0x00ff0003;
+
+ final private Handler mHandler = new Handler();
+
+ final private Runnable mRequestFocus = new Runnable() {
+ public void run() {
+ mExpandableList.focusableViewAvailable(mExpandableList);
+ }
+ };
+
+ final private AdapterView.OnItemClickListener mOnClickListener = new AdapterView.OnItemClickListener() {
+ public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
+ onListItemClick((ExpandableListView) parent, v, position, id);
+ }
+ };
+
+ ExpandableListAdapter mAdapter;
+ ExpandableListView mExpandableList;
+ boolean mFinishedStart = false;
+ View mEmptyView;
+ TextView mStandardEmptyView;
+ View mProgressContainer;
+ View mExpandableListContainer;
+ CharSequence mEmptyText;
+ boolean mExpandableListShown;
+
+ public ExpandableListFragment() {
+ }
+
+ /**
+ * Provide default implementation to return a simple list view. Subclasses can override to
+ * replace with their own layout. If doing so, the returned view hierarchy <em>must</em> have a
+ * ListView whose id is {@link android.R.id#list android.R.id.list} and can optionally have a
+ * sibling view id {@link android.R.id#empty android.R.id.empty} that is to be shown when the
+ * list is empty.
+ *
+ * <p>
+ * If you are overriding this method with your own custom content, consider including the
+ * standard layout {@link android.R.layout#list_content} in your layout file, so that you
+ * continue to retain all of the standard behavior of ListFragment. In particular, this is
+ * currently the only way to have the built-in indeterminant progress state be shown.
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final Context context = getActivity();
+
+ FrameLayout root = new FrameLayout(context);
+
+ // ------------------------------------------------------------------
+
+ LinearLayout pframe = new LinearLayout(context);
+ pframe.setId(INTERNAL_PROGRESS_CONTAINER_ID);
+ pframe.setOrientation(LinearLayout.VERTICAL);
+ pframe.setVisibility(View.GONE);
+ pframe.setGravity(Gravity.CENTER);
+
+ ProgressBar progress = new ProgressBar(context, null, android.R.attr.progressBarStyleLarge);
+ pframe.addView(progress, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+
+ root.addView(pframe, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+
+ // ------------------------------------------------------------------
+
+ FrameLayout lframe = new FrameLayout(context);
+ lframe.setId(INTERNAL_LIST_CONTAINER_ID);
+
+ TextView tv = new TextView(getActivity());
+ tv.setId(INTERNAL_EMPTY_ID);
+ tv.setGravity(Gravity.CENTER);
+ lframe.addView(tv, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+
+ ExpandableListView lv = new ExpandableListView(getActivity());
+ lv.setId(android.R.id.list);
+ lv.setDrawSelectorOnTop(false);
+ lframe.addView(lv, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+
+ root.addView(lframe, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+
+ // ------------------------------------------------------------------
+
+ root.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+
+ return root;
+ }
+
+ /**
+ * Attach to list view once the view hierarchy has been created.
+ */
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ ensureList();
+ }
+
+ /**
+ * Detach from list view.
+ */
+ @Override
+ public void onDestroyView() {
+ mHandler.removeCallbacks(mRequestFocus);
+ mExpandableList = null;
+ mExpandableListShown = false;
+ mEmptyView = mProgressContainer = mExpandableListContainer = null;
+ mStandardEmptyView = null;
+ super.onDestroyView();
+ }
+
+ /**
+ * This method will be called when an item in the list is selected. Subclasses should override.
+ * Subclasses can call getListView().getItemAtPosition(position) if they need to access the data
+ * associated with the selected item.
+ *
+ * @param l
+ * The ListView where the click happened
+ * @param v
+ * The view that was clicked within the ListView
+ * @param position
+ * The position of the view in the list
+ * @param id
+ * The row id of the item that was clicked
+ */
+ public void onListItemClick(ExpandableListView l, View v, int position, long id) {
+ }
+
+ /**
+ * Provide the cursor for the list view.
+ */
+ public void setListAdapter(ExpandableListAdapter adapter) {
+ boolean hadAdapter = mAdapter != null;
+ mAdapter = adapter;
+ if (mExpandableList != null) {
+ mExpandableList.setAdapter(adapter);
+ if (!mExpandableListShown && !hadAdapter) {
+ // The list was hidden, and previously didn't have an
+ // adapter. It is now time to show it.
+ setListShown(true, getView().getWindowToken() != null);
+ }
+ }
+ }
+
+ /**
+ * Set the currently selected list item to the specified position with the adapter's data
+ *
+ * @param position
+ */
+ public void setSelection(int position) {
+ ensureList();
+ mExpandableList.setSelection(position);
+ }
+
+ /**
+ * Get the position of the currently selected list item.
+ */
+ public int getSelectedItemPosition() {
+ ensureList();
+ return mExpandableList.getSelectedItemPosition();
+ }
+
+ /**
+ * Get the cursor row ID of the currently selected list item.
+ */
+ public long getSelectedItemId() {
+ ensureList();
+ return mExpandableList.getSelectedItemId();
+ }
+
+ /**
+ * Get the activity's list view widget.
+ */
+ public ExpandableListView getListView() {
+ ensureList();
+ return mExpandableList;
+ }
+
+ /**
+ * The default content for a ListFragment has a TextView that can be shown when the list is
+ * empty. If you would like to have it shown, call this method to supply the text it should use.
+ */
+ public void setEmptyText(CharSequence text) {
+ ensureList();
+ if (mStandardEmptyView == null) {
+ throw new IllegalStateException("Can't be used with a custom content view");
+ }
+ mStandardEmptyView.setText(text);
+ if (mEmptyText == null) {
+ mExpandableList.setEmptyView(mStandardEmptyView);
+ }
+ mEmptyText = text;
+ }
+
+ /**
+ * Control whether the list is being displayed. You can make it not displayed if you are waiting
+ * for the initial data to show in it. During this time an indeterminant progress indicator will
+ * be shown instead.
+ *
+ * <p>
+ * Applications do not normally need to use this themselves. The default behavior of
+ * ListFragment is to start with the list not being shown, only showing it once an adapter is
+ * given with {@link #setListAdapter(ListAdapter)}. If the list at that point had not been
+ * shown, when it does get shown it will be do without the user ever seeing the hidden state.
+ *
+ * @param shown
+ * If true, the list view is shown; if false, the progress indicator. The initial
+ * value is true.
+ */
+ public void setListShown(boolean shown) {
+ setListShown(shown, true);
+ }
+
+ /**
+ * Like {@link #setListShown(boolean)}, but no animation is used when transitioning from the
+ * previous state.
+ */
+ public void setListShownNoAnimation(boolean shown) {
+ setListShown(shown, false);
+ }
+
+ /**
+ * Control whether the list is being displayed. You can make it not displayed if you are waiting
+ * for the initial data to show in it. During this time an indeterminant progress indicator will
+ * be shown instead.
+ *
+ * @param shown
+ * If true, the list view is shown; if false, the progress indicator. The initial
+ * value is true.
+ * @param animate
+ * If true, an animation will be used to transition to the new state.
+ */
+ private void setListShown(boolean shown, boolean animate) {
+ ensureList();
+ if (mProgressContainer == null) {
+ throw new IllegalStateException("Can't be used with a custom content view");
+ }
+ if (mExpandableListShown == shown) {
+ return;
+ }
+ mExpandableListShown = shown;
+ if (shown) {
+ if (animate) {
+ mProgressContainer.startAnimation(AnimationUtils.loadAnimation(getActivity(),
+ android.R.anim.fade_out));
+ mExpandableListContainer.startAnimation(AnimationUtils.loadAnimation(getActivity(),
+ android.R.anim.fade_in));
+ } else {
+ mProgressContainer.clearAnimation();
+ mExpandableListContainer.clearAnimation();
+ }
+ mProgressContainer.setVisibility(View.GONE);
+ mExpandableListContainer.setVisibility(View.VISIBLE);
+ } else {
+ if (animate) {
+ mProgressContainer.startAnimation(AnimationUtils.loadAnimation(getActivity(),
+ android.R.anim.fade_in));
+ mExpandableListContainer.startAnimation(AnimationUtils.loadAnimation(getActivity(),
+ android.R.anim.fade_out));
+ } else {
+ mProgressContainer.clearAnimation();
+ mExpandableListContainer.clearAnimation();
+ }
+ mProgressContainer.setVisibility(View.VISIBLE);
+ mExpandableListContainer.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Get the ListAdapter associated with this activity's ListView.
+ */
+ public ExpandableListAdapter getListAdapter() {
+ return mAdapter;
+ }
+
+ private void ensureList() {
+ if (mExpandableList != null) {
+ return;
+ }
+ View root = getView();
+ if (root == null) {
+ throw new IllegalStateException("Content view not yet created");
+ }
+ if (root instanceof ExpandableListView) {
+ mExpandableList = (ExpandableListView) root;
+ } else {
+ mStandardEmptyView = (TextView) root.findViewById(INTERNAL_EMPTY_ID);
+ if (mStandardEmptyView == null) {
+ mEmptyView = root.findViewById(android.R.id.empty);
+ } else {
+ mStandardEmptyView.setVisibility(View.GONE);
+ }
+ mProgressContainer = root.findViewById(INTERNAL_PROGRESS_CONTAINER_ID);
+ mExpandableListContainer = root.findViewById(INTERNAL_LIST_CONTAINER_ID);
+ View rawExpandableListView = root.findViewById(android.R.id.list);
+ if (!(rawExpandableListView instanceof ExpandableListView)) {
+ if (rawExpandableListView == null) {
+ throw new RuntimeException(
+ "Your content must have a ListView whose id attribute is "
+ + "'android.R.id.list'");
+ }
+ throw new RuntimeException(
+ "Content has view with id attribute 'android.R.id.list' "
+ + "that is not a ListView class");
+ }
+ mExpandableList = (ExpandableListView) rawExpandableListView;
+ if (mEmptyView != null) {
+ mExpandableList.setEmptyView(mEmptyView);
+ } else if (mEmptyText != null) {
+ mStandardEmptyView.setText(mEmptyText);
+ mExpandableList.setEmptyView(mStandardEmptyView);
+ }
+ }
+ mExpandableListShown = true;
+ mExpandableList.setOnItemClickListener(mOnClickListener);
+ if (mAdapter != null) {
+ ExpandableListAdapter adapter = mAdapter;
+ mAdapter = null;
+ setListAdapter(adapter);
+ } else {
+ // We are starting without an adapter, so assume we won't
+ // have our data right away and start with the progress indicator.
+ if (mProgressContainer != null) {
+ setListShown(false, false);
+ }
+ }
+ mHandler.post(mRequestFocus);
+ }
+
+ /**
+ * Override this to populate the context menu when an item is long pressed. menuInfo will
+ * contain an {@link android.widget.ExpandableListView.ExpandableListContextMenuInfo} whose
+ * packedPosition is a packed position that should be used with
+ * {@link ExpandableListView#getPackedPositionType(long)} and the other similar methods.
+ * <p>
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ }
+
+ /**
+ * Override this for receiving callbacks when a child has been clicked.
+ * <p>
+ * {@inheritDoc}
+ */
+ public boolean onChildClick(ExpandableListView parent, View v, int groupPosition,
+ int childPosition, long id) {
+ return false;
+ }
+
+ /**
+ * Override this for receiving callbacks when a group has been collapsed.
+ */
+ public void onGroupCollapse(int groupPosition) {
+ }
+
+ /**
+ * Override this for receiving callbacks when a group has been expanded.
+ */
+ public void onGroupExpand(int groupPosition) {
+ }
+
+ // /**
+ // * Ensures the expandable list view has been created before Activity restores all
+ // * of the view states.
+ // *
+ // *@see Activity#onRestoreInstanceState(Bundle)
+ // */
+ // @Override
+ // protected void onRestoreInstanceState(Bundle state) {
+ // ensureList();
+ // super.onRestoreInstanceState(state);
+ // }
+
+ /**
+ * Updates the screen state (current list and other views) when the content changes.
+ *
+ * @see Activity#onContentChanged()
+ */
+
+ public void onContentChanged() {
+ // super.onContentChanged();
+ View emptyView = getView().findViewById(android.R.id.empty);
+ mExpandableList = (ExpandableListView) getView().findViewById(android.R.id.list);
+ if (mExpandableList == null) {
+ throw new RuntimeException(
+ "Your content must have a ExpandableListView whose id attribute is "
+ + "'android.R.id.list'");
+ }
+ if (emptyView != null) {
+ mExpandableList.setEmptyView(emptyView);
+ }
+ mExpandableList.setOnChildClickListener(this);
+ mExpandableList.setOnGroupExpandListener(this);
+ mExpandableList.setOnGroupCollapseListener(this);
+
+ if (mFinishedStart) {
+ setListAdapter(mAdapter);
+ }
+ mFinishedStart = true;
+ }
+
+ /**
+ * Get the activity's expandable list view widget. This can be used to get the selection, set
+ * the selection, and many other useful functions.
+ *
+ * @see ExpandableListView
+ */
+ public ExpandableListView getExpandableListView() {
+ ensureList();
+ return mExpandableList;
+ }
+
+ /**
+ * Get the ExpandableListAdapter associated with this activity's ExpandableListView.
+ */
+ public ExpandableListAdapter getExpandableListAdapter() {
+ return mAdapter;
+ }
+
+ /**
+ * Gets the ID of the currently selected group or child.
+ *
+ * @return The ID of the currently selected group or child.
+ */
+ public long getSelectedId() {
+ return mExpandableList.getSelectedId();
+ }
+
+ /**
+ * Gets the position (in packed position representation) of the currently selected group or
+ * child. Use {@link ExpandableListView#getPackedPositionType},
+ * {@link ExpandableListView#getPackedPositionGroup}, and
+ * {@link ExpandableListView#getPackedPositionChild} to unpack the returned packed position.
+ *
+ * @return A packed position representation containing the currently selected group or child's
+ * position and type.
+ */
+ public long getSelectedPosition() {
+ return mExpandableList.getSelectedPosition();
+ }
+
+ /**
+ * Sets the selection to the specified child. If the child is in a collapsed group, the group
+ * will only be expanded and child subsequently selected if shouldExpandGroup is set to true,
+ * otherwise the method will return false.
+ *
+ * @param groupPosition
+ * The position of the group that contains the child.
+ * @param childPosition
+ * The position of the child within the group.
+ * @param shouldExpandGroup
+ * Whether the child's group should be expanded if it is collapsed.
+ * @return Whether the selection was successfully set on the child.
+ */
+ public boolean setSelectedChild(int groupPosition, int childPosition, boolean shouldExpandGroup) {
+ return mExpandableList.setSelectedChild(groupPosition, childPosition, shouldExpandGroup);
+ }
+
+ /**
+ * Sets the selection to the specified group.
+ *
+ * @param groupPosition
+ * The position of the group that should be selected.
+ */
+ public void setSelectedGroup(int groupPosition) {
+ mExpandableList.setSelectedGroup(groupPosition);
+ }
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/ImportKeysListLoader.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/ImportKeysListLoader.java
new file mode 100644
index 000000000..198140e0c
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/ImportKeysListLoader.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * 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.sufficientlysecure.keychain.ui.widget;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.spongycastle.openpgp.PGPKeyRing;
+import org.spongycastle.openpgp.PGPObjectFactory;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPPublicKeyRingCollection;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.spongycastle.openpgp.PGPSecretKeyRingCollection;
+import org.spongycastle.openpgp.PGPUtil;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.helper.PgpConversionHelper;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.util.InputData;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.util.PositionAwareInputStream;
+import org.sufficientlysecure.keychain.R;
+
+import android.content.Context;
+import android.support.v4.content.AsyncTaskLoader;
+
+/**
+ * A custom Loader to search for bad adware apps, based on
+ * https://github.com/brosmike/AirPush-Detector. Daniel Bjorge licensed it under Apachev2 after
+ * asking him by mail.
+ */
+public class ImportKeysListLoader extends AsyncTaskLoader<List<Map<String, String>>> {
+ public static final String MAP_ATTR_USER_ID = "user_id";
+ public static final String MAP_ATTR_FINGERPINT = "fingerprint";
+
+ ArrayList<Map<String, String>> data = new ArrayList<Map<String, String>>();
+
+ Context mContext;
+ List<String> mItems;
+
+ byte[] mKeyringBytes;
+ String mImportFilename;
+
+ public ImportKeysListLoader(Context context, byte[] keyringBytes, String importFilename) {
+ super(context);
+ this.mContext = context;
+ this.mKeyringBytes = keyringBytes;
+ this.mImportFilename = importFilename;
+ }
+
+ @Override
+ public List<Map<String, String>> loadInBackground() {
+ InputData inputData = null;
+ if (mKeyringBytes != null) {
+ inputData = new InputData(new ByteArrayInputStream(mKeyringBytes), mKeyringBytes.length);
+ } else {
+ try {
+ inputData = new InputData(new FileInputStream(mImportFilename),
+ mImportFilename.length());
+ } catch (FileNotFoundException e) {
+ Log.e(Constants.TAG, "Failed to init FileInputStream!", e);
+ }
+ }
+
+ generateListOfKeyrings(inputData);
+
+ return data;
+ }
+
+ @Override
+ protected void onReset() {
+ super.onReset();
+
+ // Ensure the loader is stopped
+ onStopLoading();
+ }
+
+ @Override
+ protected void onStartLoading() {
+ forceLoad();
+ }
+
+ @Override
+ protected void onStopLoading() {
+ cancelLoad();
+ }
+
+ @Override
+ public void deliverResult(List<Map<String, String>> data) {
+ super.deliverResult(data);
+ }
+
+ /**
+ * Similar to PGPMain.importKeyRings
+ *
+ * @param keyringBytes
+ * @return
+ */
+ private void generateListOfKeyrings(InputData inputData) {
+ PositionAwareInputStream progressIn = new PositionAwareInputStream(
+ inputData.getInputStream());
+
+ // need to have access to the bufferedInput, so we can reuse it for the possible
+ // PGPObject chunks after the first one, e.g. files with several consecutive ASCII
+ // armour blocks
+ BufferedInputStream bufferedInput = new BufferedInputStream(progressIn);
+ try {
+
+ // read all available blocks... (asc files can contain many blocks with BEGIN END)
+ while (bufferedInput.available() > 0) {
+ InputStream in = PGPUtil.getDecoderStream(bufferedInput);
+ PGPObjectFactory objectFactory = new PGPObjectFactory(in);
+
+ // go through all objects in this block
+ Object obj;
+ while ((obj = objectFactory.nextObject()) != null) {
+ Log.d(Constants.TAG, "Found class: " + obj.getClass());
+
+ if (obj instanceof PGPKeyRing) {
+ PGPKeyRing newKeyring = (PGPKeyRing) obj;
+ addToData(newKeyring);
+ } else {
+ Log.e(Constants.TAG, "Object not recognized as PGPKeyRing!");
+ }
+ }
+ }
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "Exception on parsing key file!", e);
+ }
+ }
+
+ private void addToData(PGPKeyRing keyring) {
+ String userId = PgpHelper.getMainUserId(keyring.getPublicKey());
+
+ if (keyring instanceof PGPSecretKeyRing) {
+ userId = mContext.getString(R.string.secretKeyring) + " " + userId;
+ }
+
+ String fingerprint = PgpHelper.convertFingerprintToHex(keyring.getPublicKey()
+ .getFingerprint());
+
+ Map<String, String> attrs = new HashMap<String, String>();
+ attrs.put(MAP_ATTR_USER_ID, userId);
+ attrs.put(MAP_ATTR_FINGERPINT, mContext.getString(R.string.fingerprint) + "\n"
+ + fingerprint);
+ data.add(attrs);
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/IntegerListPreference.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/IntegerListPreference.java
new file mode 100644
index 000000000..15b4ddc6a
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/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.sufficientlysecure.keychain.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/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyEditor.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyEditor.java
new file mode 100644
index 000000000..1122fc522
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyEditor.java
@@ -0,0 +1,236 @@
+/*
+ * 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.sufficientlysecure.keychain.ui.widget;
+
+import org.spongycastle.openpgp.PGPPublicKey;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.util.Choice;
+import org.sufficientlysecure.keychain.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, int usage) {
+ mKey = key;
+
+ mIsMasterKey = isMasterKey;
+ if (mIsMasterKey) {
+ mDeleteButton.setVisibility(View.INVISIBLE);
+ }
+
+ mAlgorithm.setText(PgpHelper.getAlgorithmInfo(key));
+ String keyId1Str = PgpHelper.getSmallFingerPrint(key.getKeyID());
+ String keyId2Str = PgpHelper.getSmallFingerPrint(key.getKeyID() >> 32);
+ mKeyId.setText(keyId1Str + " " + keyId2Str);
+
+ Vector<Choice> choices = new Vector<Choice>();
+ boolean isElGamalKey = (key.getPublicKey().getAlgorithm() == PGPPublicKey.ELGAMAL_ENCRYPT);
+ boolean isDSAKey = (key.getPublicKey().getAlgorithm() == PGPPublicKey.DSA);
+ if (!isElGamalKey) {
+ choices.add(new Choice(Id.choice.usage.sign_only, getResources().getString(
+ R.string.choice_signOnly)));
+ }
+ if (!mIsMasterKey && !isDSAKey) {
+ choices.add(new Choice(Id.choice.usage.encrypt_only, getResources().getString(
+ R.string.choice_encryptOnly)));
+ }
+ if (!isElGamalKey && !isDSAKey) {
+ 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);
+
+ // Set value in choice dropdown to key
+ int selectId = 0;
+ if (PgpHelper.isEncryptionKey(key)) {
+ if (PgpHelper.isSigningKey(key)) {
+ selectId = Id.choice.usage.sign_and_encrypt;
+ } else {
+ selectId = Id.choice.usage.encrypt_only;
+ }
+ } else {
+ // set usage if it is predefined
+ if (usage != -1) {
+ selectId = usage;
+ } 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(PgpHelper.getCreationDate(key));
+ mCreationDate.setText(DateFormat.getDateInstance().format(cal.getTime()));
+ cal = new GregorianCalendar();
+ Date date = PgpHelper.getExpiryDate(key);
+ if (date == null) {
+ setExpiryDate(null);
+ } else {
+ cal.setTime(PgpHelper.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/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyListAdapter.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyListAdapter.java
new file mode 100644
index 000000000..8e8d33886
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyListAdapter.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * 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.sufficientlysecure.keychain.ui.widget;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.OtherHelper;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.MergeCursor;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CursorTreeAdapter;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class KeyListAdapter extends CursorTreeAdapter {
+ private Context mContext;
+ private LayoutInflater mInflater;
+
+ protected int mKeyType;
+
+ private static final int CHILD_KEY = 0;
+ private static final int CHILD_USER_ID = 1;
+ private static final int CHILD_FINGERPRINT = 2;
+
+ public KeyListAdapter(Context context, Cursor groupCursor, int keyType) {
+ super(groupCursor, context);
+ mContext = context;
+ mInflater = LayoutInflater.from(context);
+ mKeyType = keyType;
+ }
+
+ /**
+ * Inflate new view for group items
+ */
+ @Override
+ public View newGroupView(Context context, Cursor cursor, boolean isExpanded, ViewGroup parent) {
+ return mInflater.inflate(R.layout.key_list_group_item, null);
+ }
+
+ /**
+ * Binds TextViews from group view to results from database group cursor.
+ */
+ @Override
+ protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) {
+ int userIdIndex = cursor.getColumnIndex(UserIds.USER_ID);
+
+ TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId);
+ mainUserId.setText(R.string.unknownUserId);
+ TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest);
+ mainUserIdRest.setText("");
+
+ String userId = cursor.getString(userIdIndex);
+ if (userId != null) {
+ String[] userIdSplit = OtherHelper.splitUserId(userId);
+
+ if (userIdSplit[1] != null) {
+ mainUserIdRest.setText(userIdSplit[1]);
+ }
+ mainUserId.setText(userIdSplit[0]);
+ }
+
+ if (mainUserId.getText().length() == 0) {
+ mainUserId.setText(R.string.unknownUserId);
+ }
+
+ if (mainUserIdRest.getText().length() == 0) {
+ mainUserIdRest.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Inflate new view for child items
+ */
+ @Override
+ public View newChildView(Context context, Cursor cursor, boolean isLastChild, ViewGroup parent) {
+ return mInflater.inflate(R.layout.key_list_child_item, null);
+ }
+
+ /**
+ * Bind TextViews from view of childs based on query results
+ */
+ @Override
+ protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) {
+ LinearLayout keyLayout = (LinearLayout) view.findViewById(R.id.keyLayout);
+ LinearLayout userIdLayout = (LinearLayout) view.findViewById(R.id.userIdLayout);
+
+ // first entry is fingerprint
+ if (cursor.getPosition() == 0) {
+ // show only userId layout
+ keyLayout.setVisibility(View.GONE);
+ userIdLayout.setVisibility(View.VISIBLE);
+
+ String fingerprint = PgpHelper.getFingerPrint(context,
+ cursor.getLong(cursor.getColumnIndex(Keys.KEY_ID)));
+ fingerprint = fingerprint.replace(" ", "\n");
+
+ TextView userId = (TextView) view.findViewById(R.id.userId);
+ if (userId == null) {
+ Log.d(Constants.TAG, "userId is null!");
+ }
+ userId.setText(context.getString(R.string.fingerprint) + "\n" + fingerprint);
+ } else {
+ // differentiate between keys and userIds in MergeCursor
+ if (cursor.getColumnIndex(Keys.KEY_ID) != -1) {
+ keyLayout.setVisibility(View.VISIBLE);
+ userIdLayout.setVisibility(View.GONE);
+
+ String keyIdStr = PgpHelper.getSmallFingerPrint(cursor.getLong(cursor
+ .getColumnIndex(Keys.KEY_ID)));
+ String algorithmStr = PgpHelper.getAlgorithmInfo(
+ cursor.getInt(cursor.getColumnIndex(Keys.ALGORITHM)),
+ cursor.getInt(cursor.getColumnIndex(Keys.KEY_SIZE)));
+
+ TextView keyId = (TextView) view.findViewById(R.id.keyId);
+ keyId.setText(keyIdStr);
+
+ TextView keyDetails = (TextView) view.findViewById(R.id.keyDetails);
+ keyDetails.setText("(" + algorithmStr + ")");
+
+ ImageView masterKeyIcon = (ImageView) view.findViewById(R.id.ic_masterKey);
+ if (cursor.getInt(cursor.getColumnIndex(Keys.IS_MASTER_KEY)) != 1) {
+ masterKeyIcon.setVisibility(View.INVISIBLE);
+ } else {
+ masterKeyIcon.setVisibility(View.VISIBLE);
+ }
+
+ ImageView encryptIcon = (ImageView) view.findViewById(R.id.ic_encryptKey);
+ if (cursor.getInt(cursor.getColumnIndex(Keys.CAN_ENCRYPT)) != 1) {
+ encryptIcon.setVisibility(View.GONE);
+ } else {
+ encryptIcon.setVisibility(View.VISIBLE);
+ }
+
+ ImageView signIcon = (ImageView) view.findViewById(R.id.ic_signKey);
+ if (cursor.getInt(cursor.getColumnIndex(Keys.CAN_SIGN)) != 1) {
+ signIcon.setVisibility(View.GONE);
+ } else {
+ signIcon.setVisibility(View.VISIBLE);
+ }
+ } else {
+ keyLayout.setVisibility(View.GONE);
+ userIdLayout.setVisibility(View.VISIBLE);
+
+ String userIdStr = cursor.getString(cursor.getColumnIndex(UserIds.USER_ID));
+
+ TextView userId = (TextView) view.findViewById(R.id.userId);
+ userId.setText(userIdStr);
+ }
+ }
+ }
+
+ /**
+ * Given the group cursor, we start cursors for a fingerprint, keys, and userIds, which are
+ * merged together and build the child cursor
+ */
+ @Override
+ protected Cursor getChildrenCursor(Cursor groupCursor) {
+ final long keyRingRowId = groupCursor.getLong(groupCursor.getColumnIndex(BaseColumns._ID));
+
+ Cursor fingerprintCursor = getChildCursor(keyRingRowId, CHILD_FINGERPRINT);
+ Cursor keyCursor = getChildCursor(keyRingRowId, CHILD_KEY);
+ Cursor userIdCursor = getChildCursor(keyRingRowId, CHILD_USER_ID);
+
+ MergeCursor mergeCursor = new MergeCursor(new Cursor[] { fingerprintCursor, keyCursor,
+ userIdCursor });
+ Log.d(Constants.TAG, "mergeCursor:" + DatabaseUtils.dumpCursorToString(mergeCursor));
+
+ return mergeCursor;
+ }
+
+ /**
+ * This builds a cursor for a specific type of children
+ *
+ * @param keyRingRowId
+ * foreign row id of the keyRing
+ * @param type
+ * @return
+ */
+ private Cursor getChildCursor(long keyRingRowId, int type) {
+ Uri uri = null;
+ String[] projection = null;
+ String sortOrder = null;
+ String selection = null;
+
+ switch (type) {
+ case CHILD_FINGERPRINT:
+ projection = new String[] { Keys._ID, Keys.KEY_ID, Keys.IS_MASTER_KEY, Keys.ALGORITHM,
+ Keys.KEY_SIZE, Keys.CAN_SIGN, Keys.CAN_ENCRYPT, };
+ sortOrder = Keys.RANK + " ASC";
+
+ // use only master key for fingerprint
+ selection = Keys.IS_MASTER_KEY + " = 1 ";
+
+ if (mKeyType == Id.type.public_key) {
+ uri = Keys.buildPublicKeysUri(String.valueOf(keyRingRowId));
+ } else {
+ uri = Keys.buildSecretKeysUri(String.valueOf(keyRingRowId));
+ }
+ break;
+
+ case CHILD_KEY:
+ projection = new String[] { Keys._ID, Keys.KEY_ID, Keys.IS_MASTER_KEY, Keys.ALGORITHM,
+ Keys.KEY_SIZE, Keys.CAN_SIGN, Keys.CAN_ENCRYPT, };
+ sortOrder = Keys.RANK + " ASC";
+
+ if (mKeyType == Id.type.public_key) {
+ uri = Keys.buildPublicKeysUri(String.valueOf(keyRingRowId));
+ } else {
+ uri = Keys.buildSecretKeysUri(String.valueOf(keyRingRowId));
+ }
+
+ break;
+
+ case CHILD_USER_ID:
+ projection = new String[] { UserIds._ID, UserIds.USER_ID, UserIds.RANK, };
+ sortOrder = UserIds.RANK + " ASC";
+
+ // not the main user id
+ selection = UserIds.RANK + " > 0 ";
+
+ if (mKeyType == Id.type.public_key) {
+ uri = UserIds.buildPublicUserIdsUri(String.valueOf(keyRingRowId));
+ } else {
+ uri = UserIds.buildSecretUserIdsUri(String.valueOf(keyRingRowId));
+ }
+
+ break;
+
+ default:
+ return null;
+
+ }
+
+ return mContext.getContentResolver().query(uri, projection, selection, null, sortOrder);
+ }
+
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyServerEditor.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyServerEditor.java
new file mode 100644
index 000000000..7c9ba5fe6
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/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.sufficientlysecure.keychain.ui.widget;
+
+import org.sufficientlysecure.keychain.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/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SectionView.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SectionView.java
new file mode 100644
index 000000000..7142e4426
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SectionView.java
@@ -0,0 +1,327 @@
+/*
+ * 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.sufficientlysecure.keychain.ui.widget;
+
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.PgpConversionHelper;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.service.PassphraseCacheService;
+import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment;
+import org.sufficientlysecure.keychain.ui.widget.Editor.EditorListener;
+import org.sufficientlysecure.keychain.util.Choice;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.Messenger;
+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 java.util.Iterator;
+import java.util.Vector;
+
+public class SectionView extends LinearLayout implements OnClickListener, EditorListener {
+ private LayoutInflater mInflater;
+ private View mAdd;
+ private ViewGroup mEditors;
+ private TextView mTitle;
+ private int mType = 0;
+
+ private Choice mNewKeyAlgorithmChoice;
+ private int mNewKeySize;
+
+ private SherlockFragmentActivity mActivity;
+
+ private ProgressDialogFragment mGeneratingDialog;
+
+ public SectionView(Context context) {
+ super(context);
+ mActivity = (SherlockFragmentActivity) context;
+ }
+
+ public SectionView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mActivity = (SherlockFragmentActivity) context;
+ }
+
+ 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);
+
+ boolean wouldBeMasterKey = (mEditors.getChildCount() == 0);
+
+ final Spinner algorithm = (Spinner) view.findViewById(R.id.create_key_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.create_key_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, Vector<Integer> usages) {
+ if (mType != Id.type.key) {
+ return;
+ }
+
+ mEditors.removeAllViews();
+
+ // go through all keys and set view based on them
+ for (int i = 0; i < list.size(); i++) {
+ KeyEditor view = (KeyEditor) mInflater.inflate(R.layout.edit_key_key_item, mEditors,
+ false);
+ view.setEditorListener(this);
+ boolean isMasterKey = (mEditors.getChildCount() == 0);
+ view.setValue(list.get(i), isMasterKey, usages.get(i));
+ mEditors.addView(view);
+ }
+
+ this.updateEditorsVisible();
+ }
+
+ private void createKey() {
+ // Send all information needed to service to edit key in other thread
+ Intent intent = new Intent(mActivity, KeychainIntentService.class);
+
+ intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_GENERATE_KEY);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ String passPhrase;
+ if (mEditors.getChildCount() > 0) {
+ PGPSecretKey masterKey = ((KeyEditor) mEditors.getChildAt(0)).getValue();
+ passPhrase = PassphraseCacheService
+ .getCachedPassphrase(mActivity, masterKey.getKeyID());
+
+ data.putByteArray(KeychainIntentService.GENERATE_KEY_MASTER_KEY,
+ PgpConversionHelper.PGPSecretKeyToBytes(masterKey));
+ } else {
+ passPhrase = "";
+ }
+ data.putString(KeychainIntentService.GENERATE_KEY_SYMMETRIC_PASSPHRASE, passPhrase);
+ data.putInt(KeychainIntentService.GENERATE_KEY_ALGORITHM, mNewKeyAlgorithmChoice.getId());
+ data.putInt(KeychainIntentService.GENERATE_KEY_KEY_SIZE, mNewKeySize);
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // show progress dialog
+ mGeneratingDialog = ProgressDialogFragment.newInstance(R.string.progress_generating,
+ ProgressDialog.STYLE_SPINNER);
+
+ // Message is received after generating is done in ApgService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(mActivity, mGeneratingDialog) {
+ public void handleMessage(Message message) {
+ // handle messages by standard ApgHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ // get new key from data bundle returned from service
+ Bundle data = message.getData();
+ PGPSecretKeyRing newKeyRing = (PGPSecretKeyRing) PgpConversionHelper
+ .BytesToPGPKeyRing(data.getByteArray(KeychainIntentService.RESULT_NEW_KEY));
+
+ boolean isMasterKey = (mEditors.getChildCount() == 0);
+
+ // take only the key from this ring
+ PGPSecretKey newKey = null;
+ @SuppressWarnings("unchecked")
+ Iterator<PGPSecretKey> it = newKeyRing.getSecretKeys();
+
+ if (isMasterKey) {
+ newKey = it.next();
+ } else {
+ // first one is the master key
+ it.next();
+ newKey = it.next();
+ }
+
+ // add view with new key
+ KeyEditor view = (KeyEditor) mInflater.inflate(R.layout.edit_key_key_item,
+ mEditors, false);
+ view.setEditorListener(SectionView.this);
+ view.setValue(newKey, isMasterKey, -1);
+ mEditors.addView(view);
+ SectionView.this.updateEditorsVisible();
+ }
+ };
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ mGeneratingDialog.show(mActivity.getSupportFragmentManager(), "dialog");
+
+ // start service with intent
+ mActivity.startService(intent);
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SelectKeyCursorAdapter.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SelectKeyCursorAdapter.java
new file mode 100644
index 000000000..da8ac94ee
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SelectKeyCursorAdapter.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * 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.sufficientlysecure.keychain.ui.widget;
+
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.OtherHelper;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
+import org.sufficientlysecure.keychain.R;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.support.v4.widget.CursorAdapter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.ListView;
+import android.widget.TextView;
+
+public class SelectKeyCursorAdapter extends CursorAdapter {
+
+ protected int mKeyType;
+
+ private LayoutInflater mInflater;
+ private ListView mListView;
+
+ public final static String PROJECTION_ROW_AVAILABLE = "available";
+ public final static String PROJECTION_ROW_VALID = "valid";
+
+ @SuppressWarnings("deprecation")
+ public SelectKeyCursorAdapter(Context context, ListView listView, Cursor c, int keyType) {
+ super(context, c);
+
+ mInflater = LayoutInflater.from(context);
+ mListView = listView;
+ mKeyType = keyType;
+ }
+
+ public String getUserId(int position) {
+ mCursor.moveToPosition(position);
+ return mCursor.getString(mCursor.getColumnIndex(UserIds.USER_ID));
+ }
+
+ public long getMasterKeyId(int position) {
+ mCursor.moveToPosition(position);
+ return mCursor.getLong(mCursor.getColumnIndex(KeyRings.MASTER_KEY_ID));
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ boolean valid = cursor.getInt(cursor
+ .getColumnIndex(PROJECTION_ROW_VALID)) > 0;
+
+ 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 = cursor.getString(cursor.getColumnIndex(UserIds.USER_ID));
+ if (userId != null) {
+ String[] userIdSplit = OtherHelper.splitUserId(userId);
+
+ if (userIdSplit[1] != null) {
+ mainUserIdRest.setText(userIdSplit[1]);
+ }
+ mainUserId.setText(userIdSplit[0]);
+ }
+
+ long masterKeyId = cursor.getLong(cursor.getColumnIndex(KeyRings.MASTER_KEY_ID));
+ keyId.setText(PgpHelper.getSmallFingerPrint(masterKeyId));
+
+ if (mainUserIdRest.getText().length() == 0) {
+ mainUserIdRest.setVisibility(View.GONE);
+ }
+
+ if (valid) {
+ if (mKeyType == Id.type.public_key) {
+ status.setText(R.string.canEncrypt);
+ } else {
+ status.setText(R.string.canSign);
+ }
+ } else {
+ if (cursor.getInt(cursor
+ .getColumnIndex(PROJECTION_ROW_AVAILABLE)) > 0) {
+ // has some CAN_ENCRYPT keys, but col(ROW_VALID) = 0, so must be revoked or
+ // expired
+ status.setText(R.string.expired);
+ } else {
+ status.setText(R.string.noKey);
+ }
+ }
+
+ CheckBox selected = (CheckBox) view.findViewById(R.id.selected);
+ if (mKeyType == Id.type.public_key) {
+ selected.setVisibility(View.VISIBLE);
+
+ if (!valid) {
+ mListView.setItemChecked(cursor.getPosition(), false);
+ }
+
+ selected.setChecked(mListView.isItemChecked(cursor.getPosition()));
+ selected.setEnabled(valid);
+ } else {
+ selected.setVisibility(View.GONE);
+ }
+
+ status.setText(status.getText() + " ");
+
+ view.setEnabled(valid);
+ mainUserId.setEnabled(valid);
+ mainUserIdRest.setEnabled(valid);
+ keyId.setEnabled(valid);
+ status.setEnabled(valid);
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ return mInflater.inflate(R.layout.select_key_item, null);
+ }
+
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/UserIdEditor.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/UserIdEditor.java
new file mode 100644
index 000000000..2215d29e7
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/UserIdEditor.java
@@ -0,0 +1,196 @@
+/*
+ * 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.sufficientlysecure.keychain.ui.widget;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.sufficientlysecure.keychain.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;
+
+ // see http://www.regular-expressions.info/email.html
+ // RFC 2822 if we omit the syntax using double quotes and square brackets
+ // android.util.Patterns.EMAIL_ADDRESS is only available as of Android 2.2+
+ private static final Pattern EMAIL_PATTERN = Pattern
+ .compile(
+ "[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?",
+ 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;
+ }
+}