diff options
Diffstat (limited to 'OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui')
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; + } +} |