aboutsummaryrefslogtreecommitdiffstats
path: root/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui
diff options
context:
space:
mode:
authorDominik Schürmann <dominik@dominikschuermann.de>2014-04-06 12:57:42 +0200
committerDominik Schürmann <dominik@dominikschuermann.de>2014-04-06 12:57:42 +0200
commit6d1137190529dc7add74926cea52c377883319be (patch)
treefd88b29a048f3aec1daa2a84bbaf22c0efa3663f /OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui
parent17997dd362fe62d72113a0536069d0fdb9c3211b (diff)
downloadopen-keychain-6d1137190529dc7add74926cea52c377883319be.tar.gz
open-keychain-6d1137190529dc7add74926cea52c377883319be.tar.bz2
open-keychain-6d1137190529dc7add74926cea52c377883319be.zip
Rename folder structure from OpenPGP Keychain to OpenKeychain
Diffstat (limited to 'OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui')
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyActivity.java388
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptActivity.java182
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFileFragment.java261
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java180
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptMessageFragment.java189
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DrawerActivity.java295
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyActivity.java760
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptActivity.java256
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptActivityInterface.java30
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptAsymmetricFragment.java268
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFileFragment.java380
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptMessageFragment.java259
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptSymmetricFragment.java98
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpAboutFragment.java75
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpActivity.java79
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpHtmlFragment.java75
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java489
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysClipboardFragment.java87
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java98
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java305
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysNFCFragment.java68
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysQrCodeFragment.java205
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysServerFragment.java155
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListActivity.java95
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java700
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PreferencesActivity.java387
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PreferencesKeyServerActivity.java130
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectPublicKeyActivity.java143
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectPublicKeyFragment.java350
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectSecretKeyActivity.java83
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectSecretKeyFragment.java176
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectSecretKeyLayoutFragment.java211
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java127
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewCertActivity.java253
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java277
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivityJB.java124
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyCertsFragment.java311
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java347
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/AsyncTaskResultWrapper.java46
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/HighlightQueryCursorAdapter.java65
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java186
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListEntry.java274
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListLoader.java167
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListServerLoader.java130
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyValueSpinnerAdapter.java101
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/PagerTabStripAdapter.java70
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SelectKeyCursorAdapter.java165
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/TabsAdapter.java101
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ViewKeyKeysAdapter.java175
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ViewKeyUserIdsAdapter.java181
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/BadImportKeyDialogFragment.java67
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/CreateKeyDialogFragment.java159
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/DeleteFileDialogFragment.java124
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java174
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java220
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java328
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ProgressDialogFragment.java154
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java186
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ShareNfcDialogFragment.java99
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ShareQrCodeDialogFragment.java212
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/Editor.java27
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/FixedListView.java55
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/FoldableLinearLayout.java203
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/IntegerListPreference.java94
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyEditor.java377
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyServerEditor.java82
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SectionView.java429
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/UnderlineTextView.java69
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/UserIdEditor.java265
69 files changed, 13881 insertions, 0 deletions
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyActivity.java
new file mode 100644
index 000000000..7027c114e
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyActivity.java
@@ -0,0 +1,388 @@
+/*
+ * Copyright (C) 2014 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 android.app.ProgressDialog;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Messenger;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.ActionBarActivity;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.ListView;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+import com.beardedhen.androidbootstrap.BootstrapButton;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.helper.Preferences;
+import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
+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.adapter.ViewKeyUserIdsAdapter;
+import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * Signs the specified public key with the specified secret master key
+ */
+public class CertifyKeyActivity extends ActionBarActivity implements
+ SelectSecretKeyLayoutFragment.SelectSecretKeyCallback, LoaderManager.LoaderCallbacks<Cursor> {
+ private BootstrapButton mSignButton;
+ private CheckBox mUploadKeyCheckbox;
+ private Spinner mSelectKeyserverSpinner;
+
+ private SelectSecretKeyLayoutFragment mSelectKeyFragment;
+
+ private Uri mDataUri;
+ private long mPubKeyId = 0;
+ private long mMasterKeyId = 0;
+
+ private ListView mUserIds;
+ private ViewKeyUserIdsAdapter mUserIdsAdapter;
+
+ private static final int LOADER_ID_KEYRING = 0;
+ private static final int LOADER_ID_USER_IDS = 1;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.certify_key_activity);
+
+ final ActionBar actionBar = getSupportActionBar();
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(false);
+ actionBar.setHomeButtonEnabled(false);
+
+ mSelectKeyFragment = (SelectSecretKeyLayoutFragment) getSupportFragmentManager()
+ .findFragmentById(R.id.sign_key_select_key_fragment);
+ mSelectKeyFragment.setCallback(this);
+ mSelectKeyFragment.setFilterCertify(true);
+
+ mSelectKeyserverSpinner = (Spinner) findViewById(R.id.sign_key_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);
+ mSelectKeyserverSpinner.setAdapter(adapter);
+
+ mUploadKeyCheckbox = (CheckBox) findViewById(R.id.sign_key_upload_checkbox);
+ if (!mUploadKeyCheckbox.isChecked()) {
+ mSelectKeyserverSpinner.setEnabled(false);
+ } else {
+ mSelectKeyserverSpinner.setEnabled(true);
+ }
+
+ mUploadKeyCheckbox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ if (!isChecked) {
+ mSelectKeyserverSpinner.setEnabled(false);
+ } else {
+ mSelectKeyserverSpinner.setEnabled(true);
+ }
+ }
+ });
+
+ mSignButton = (BootstrapButton) findViewById(R.id.sign_key_sign_button);
+ mSignButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (mPubKeyId != 0) {
+ if (mMasterKeyId == 0) {
+ mSelectKeyFragment.setError(getString(R.string.select_key_to_sign));
+ } else {
+ initiateSigning();
+ }
+ }
+ }
+ });
+
+ mDataUri = getIntent().getData();
+ if (mDataUri == null) {
+ Log.e(Constants.TAG, "Intent data missing. Should be Uri of key!");
+ finish();
+ return;
+ }
+ Log.e(Constants.TAG, "uri: " + mDataUri);
+
+ mUserIds = (ListView) findViewById(R.id.user_ids);
+
+ mUserIdsAdapter = new ViewKeyUserIdsAdapter(this, null, 0, true);
+ mUserIds.setAdapter(mUserIdsAdapter);
+ mUserIds.setOnItemClickListener(mUserIdsAdapter);
+
+ getSupportLoaderManager().initLoader(LOADER_ID_KEYRING, null, this);
+ getSupportLoaderManager().initLoader(LOADER_ID_USER_IDS, null, this);
+
+ }
+
+ static final String USER_IDS_SELECTION = UserIds.IS_REVOKED + " = 0";
+
+ static final String[] KEYRING_PROJECTION =
+ new String[] {
+ KeyRings._ID,
+ KeyRings.MASTER_KEY_ID,
+ KeyRings.FINGERPRINT,
+ KeyRings.USER_ID,
+ };
+ static final int INDEX_MASTER_KEY_ID = 1;
+ static final int INDEX_FINGERPRINT = 2;
+ static final int INDEX_USER_ID = 3;
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ switch(id) {
+ case LOADER_ID_KEYRING: {
+ Uri uri = KeyRings.buildUnifiedKeyRingUri(mDataUri);
+ return new CursorLoader(this, uri, KEYRING_PROJECTION, null, null, null);
+ }
+ case LOADER_ID_USER_IDS: {
+ Uri uri = UserIds.buildUserIdsUri(mDataUri);
+ return new CursorLoader(this, uri,
+ ViewKeyUserIdsAdapter.USER_IDS_PROJECTION, USER_IDS_SELECTION, null, null);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ switch(loader.getId()) {
+ case LOADER_ID_KEYRING:
+ // the first key here is our master key
+ if (data.moveToFirst()) {
+ // TODO: put findViewById in onCreate!
+ mPubKeyId = data.getLong(INDEX_MASTER_KEY_ID);
+ String keyIdStr = PgpKeyHelper.convertKeyIdToHexShort(mPubKeyId);
+ ((TextView) findViewById(R.id.key_id)).setText(keyIdStr);
+
+ String mainUserId = data.getString(INDEX_USER_ID);
+ ((TextView) findViewById(R.id.main_user_id)).setText(mainUserId);
+
+ byte[] fingerprintBlob = data.getBlob(INDEX_FINGERPRINT);
+ String fingerprint = PgpKeyHelper.convertFingerprintToHex(fingerprintBlob);
+ ((TextView) findViewById(R.id.fingerprint))
+ .setText(PgpKeyHelper.colorizeFingerprint(fingerprint));
+ }
+ break;
+ case LOADER_ID_USER_IDS:
+ mUserIdsAdapter.swapCursor(data);
+ break;
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ switch(loader.getId()) {
+ case LOADER_ID_USER_IDS:
+ mUserIdsAdapter.swapCursor(null);
+ break;
+ }
+ }
+
+ /**
+ * handles the UI bits of the signing process on the UI thread
+ */
+ private void initiateSigning() {
+ PGPPublicKeyRing pubring = ProviderHelper.getPGPPublicKeyRing(this, mPubKeyId);
+ if (pubring != null) {
+ // if we have already signed this key, dont bother doing it again
+ boolean alreadySigned = false;
+
+ /* todo: reconsider this at a later point when certs are in the db
+ @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) {
+ PassphraseDialogFragment.show(this, mMasterKeyId,
+ new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) {
+ startSigning();
+ }
+ }
+ });
+ // bail out; need to wait until the user has entered the passphrase before trying again
+ return;
+ } else {
+ startSigning();
+ }
+ } else {
+ Toast.makeText(this, R.string.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() {
+
+ // Bail out if there is not at least one user id selected
+ ArrayList<String> userIds = mUserIdsAdapter.getSelectedUserIds();
+ if (userIds.isEmpty()) {
+ Toast.makeText(CertifyKeyActivity.this, "No User IDs to sign selected!",
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // Send all information needed to service to sign key in other thread
+ Intent intent = new Intent(this, KeychainIntentService.class);
+
+ intent.setAction(KeychainIntentService.ACTION_CERTIFY_KEYRING);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ data.putLong(KeychainIntentService.CERTIFY_KEY_MASTER_KEY_ID, mMasterKeyId);
+ data.putLong(KeychainIntentService.CERTIFY_KEY_PUB_KEY_ID, mPubKeyId);
+ data.putStringArrayList(KeychainIntentService.CERTIFY_KEY_UIDS, userIds);
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after signing is done in KeychainIntentService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
+ getString(R.string.progress_signing), ProgressDialog.STYLE_SPINNER) {
+ public void handleMessage(Message message) {
+ // handle messages by standard KeychainIntentServiceHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+
+ Toast.makeText(CertifyKeyActivity.this, R.string.key_sign_success,
+ Toast.LENGTH_SHORT).show();
+
+ // check if we need to send the key to the server or not
+ if (mUploadKeyCheckbox.isChecked()) {
+ // upload the newly signed key to the keyserver
+ uploadKey();
+ } else {
+ setResult(RESULT_OK);
+ 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.setAction(KeychainIntentService.ACTION_UPLOAD_KEYRING);
+
+ // set data uri as path to keyring
+ intent.setData(mDataUri);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ Spinner keyServer = (Spinner) findViewById(R.id.sign_key_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 KeychainIntentService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
+ getString(R.string.progress_exporting), ProgressDialog.STYLE_HORIZONTAL) {
+ public void handleMessage(Message message) {
+ // handle messages by standard KeychainIntentServiceHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ Toast.makeText(CertifyKeyActivity.this, R.string.key_send_success,
+ Toast.LENGTH_SHORT).show();
+
+ setResult(RESULT_OK);
+ 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);
+ }
+
+ /**
+ * callback from select key fragment
+ */
+ @Override
+ public void onKeySelected(long secretKeyId) {
+ mMasterKeyId = secretKeyId;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptActivity.java
new file mode 100644
index 000000000..8533e9072
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptActivity.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2012-2014 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 android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.view.PagerTabStrip;
+import android.support.v4.view.ViewPager;
+import android.widget.Toast;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.helper.ActionBarHelper;
+import org.sufficientlysecure.keychain.helper.FileHelper;
+import org.sufficientlysecure.keychain.pgp.PgpHelper;
+import org.sufficientlysecure.keychain.ui.adapter.PagerTabStripAdapter;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.util.regex.Matcher;
+
+public class DecryptActivity extends DrawerActivity {
+
+ /* Intents */
+ public static final String ACTION_DECRYPT = Constants.INTENT_PREFIX + "DECRYPT";
+
+ /* EXTRA keys for input */
+ public static final String EXTRA_TEXT = "text";
+
+ ViewPager mViewPager;
+ PagerTabStrip mPagerTabStrip;
+ PagerTabStripAdapter mTabsAdapter;
+
+ Bundle mMessageFragmentBundle = new Bundle();
+ Bundle mFileFragmentBundle = new Bundle();
+ int mSwitchToTab = PAGER_TAB_MESSAGE;
+
+ private static final int PAGER_TAB_MESSAGE = 0;
+ private static final int PAGER_TAB_FILE = 1;
+
+ private void initView() {
+ mViewPager = (ViewPager) findViewById(R.id.decrypt_pager);
+ mPagerTabStrip = (PagerTabStrip) findViewById(R.id.decrypt_pager_tab_strip);
+
+ mTabsAdapter = new PagerTabStripAdapter(this);
+ mViewPager.setAdapter(mTabsAdapter);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.decrypt_activity);
+
+ // set actionbar without home button if called from another app
+ ActionBarHelper.setBackButton(this);
+
+ initView();
+
+ setupDrawerNavigation(savedInstanceState);
+
+ // Handle intent actions, maybe changes the bundles
+ handleActions(getIntent());
+
+ mTabsAdapter.addTab(DecryptMessageFragment.class,
+ mMessageFragmentBundle, getString(R.string.label_message));
+ mTabsAdapter.addTab(DecryptFileFragment.class,
+ mFileFragmentBundle, getString(R.string.label_file));
+ mViewPager.setCurrentItem(mSwitchToTab);
+ }
+
+
+ /**
+ * 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 Keychain Decrypt 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
+ // executeServiceMethod 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;
+ }
+ } else if (Intent.ACTION_VIEW.equals(action)) {
+ // Android's Action when opening file associated to Keychain (see AndroidManifest.xml)
+
+ // override action
+ action = ACTION_DECRYPT;
+ }
+
+ String textData = extras.getString(EXTRA_TEXT);
+
+ /**
+ * Main Actions
+ */
+ if (ACTION_DECRYPT.equals(action) && textData != null) {
+ Log.d(Constants.TAG, "textData not null, matching text ...");
+ Matcher matcher = PgpHelper.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", " ");
+
+ mMessageFragmentBundle.putString(DecryptMessageFragment.ARG_CIPHERTEXT, textData);
+ mSwitchToTab = PAGER_TAB_MESSAGE;
+ } else {
+ matcher = PgpHelper.PGP_CLEARTEXT_SIGNATURE.matcher(textData);
+ if (matcher.matches()) {
+ Log.d(Constants.TAG, "PGP_CLEARTEXT_SIGNATURE matched");
+ textData = matcher.group(1);
+ // replace non breakable spaces
+ textData = textData.replaceAll("\\xa0", " ");
+
+ mMessageFragmentBundle.putString(DecryptMessageFragment.ARG_CIPHERTEXT, textData);
+ mSwitchToTab = PAGER_TAB_MESSAGE;
+ } else {
+ Log.d(Constants.TAG, "Nothing matched!");
+ }
+ }
+ } else if (ACTION_DECRYPT.equals(action) && uri != null) {
+ // get file path from uri
+ String path = FileHelper.getPath(this, uri);
+
+ if (path != null) {
+ mFileFragmentBundle.putString(DecryptFileFragment.ARG_FILENAME, path);
+ mSwitchToTab = PAGER_TAB_FILE;
+ } else {
+ Log.e(Constants.TAG,
+ "Direct binary data without actual file in filesystem is not supported. " +
+ "Please use the Remote Service API!");
+ Toast.makeText(this, R.string.error_only_files_are_supported, Toast.LENGTH_LONG)
+ .show();
+ // end activity
+ finish();
+ }
+ } else {
+ Log.e(Constants.TAG,
+ "Include the extra 'text' or an Uri with setData() in your Intent!");
+ }
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFileFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFileFragment.java
new file mode 100644
index 000000000..492c0cf29
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFileFragment.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.app.Activity;
+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.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.EditText;
+
+import com.beardedhen.androidbootstrap.BootstrapButton;
+import com.devspark.appmsg.AppMsg;
+
+import org.openintents.openpgp.OpenPgpSignatureResult;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.helper.FileHelper;
+import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyResult;
+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 java.io.File;
+
+public class DecryptFileFragment extends DecryptFragment {
+ public static final String ARG_FILENAME = "filename";
+
+ private static final int RESULT_CODE_FILE = 0x00007003;
+
+ // view
+ private EditText mFilename;
+ private CheckBox mDeleteAfter;
+ private BootstrapButton mBrowse;
+ private BootstrapButton mDecryptButton;
+
+ private String mInputFilename = null;
+ private String mOutputFilename = null;
+
+ private FileDialogFragment mFileDialog;
+
+ /**
+ * Inflate the layout for this fragment
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.decrypt_file_fragment, container, false);
+
+ mFilename = (EditText) view.findViewById(R.id.decrypt_file_filename);
+ mBrowse = (BootstrapButton) view.findViewById(R.id.decrypt_file_browse);
+ mDeleteAfter = (CheckBox) view.findViewById(R.id.decrypt_file_delete_after_decryption);
+ mDecryptButton = (BootstrapButton) view.findViewById(R.id.decrypt_file_action_decrypt);
+ mBrowse.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ FileHelper.openFile(DecryptFileFragment.this, mFilename.getText().toString(), "*/*",
+ RESULT_CODE_FILE);
+ }
+ });
+ mDecryptButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ decryptAction();
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ String filename = getArguments().getString(ARG_FILENAME);
+ if (filename != null) {
+ mFilename.setText(filename);
+ }
+ }
+
+ 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 decryptAction() {
+ String currentFilename = mFilename.getText().toString();
+ if (mInputFilename == null || !mInputFilename.equals(currentFilename)) {
+ guessOutputFilename();
+ }
+
+ if (mInputFilename.equals("")) {
+ AppMsg.makeText(getActivity(), R.string.no_file_selected, AppMsg.STYLE_ALERT).show();
+ return;
+ }
+
+ if (mInputFilename.startsWith("file")) {
+ File file = new File(mInputFilename);
+ if (!file.exists() || !file.isFile()) {
+ AppMsg.makeText(
+ getActivity(),
+ getString(R.string.error_message,
+ getString(R.string.error_file_not_found)), AppMsg.STYLE_ALERT)
+ .show();
+ return;
+ }
+ }
+
+ askForOutputFilename();
+ }
+
+ 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(null);
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(returnHandler);
+
+ mFileDialog = FileDialogFragment.newInstance(messenger,
+ getString(R.string.title_decrypt_to_file),
+ getString(R.string.specify_file_to_decrypt_to), mOutputFilename, null);
+
+ mFileDialog.show(getActivity().getSupportFragmentManager(), "fileDialog");
+ }
+
+ @Override
+ protected void decryptStart(String passphrase) {
+ Log.d(Constants.TAG, "decryptStart");
+
+ // Send all information needed to service to decrypt in other thread
+ Intent intent = new Intent(getActivity(), KeychainIntentService.class);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ intent.setAction(KeychainIntentService.ACTION_DECRYPT_VERIFY);
+
+ // data
+ data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_URI);
+
+ Log.d(Constants.TAG, "mInputFilename=" + mInputFilename + ", mOutputFilename="
+ + mOutputFilename);
+
+ data.putString(KeychainIntentService.ENCRYPT_INPUT_FILE, mInputFilename);
+ data.putString(KeychainIntentService.ENCRYPT_OUTPUT_FILE, mOutputFilename);
+
+ data.putString(KeychainIntentService.DECRYPT_PASSPHRASE, passphrase);
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after encrypting is done in KeychainIntentService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(getActivity(),
+ getString(R.string.progress_decrypting), ProgressDialog.STYLE_HORIZONTAL) {
+ public void handleMessage(Message message) {
+ // handle messages by standard KeychainIntentServiceHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ // get returned data bundle
+ Bundle returnData = message.getData();
+
+ PgpDecryptVerifyResult decryptVerifyResult =
+ returnData.getParcelable(KeychainIntentService.RESULT_DECRYPT_VERIFY_RESULT);
+
+ if (PgpDecryptVerifyResult.KEY_PASSHRASE_NEEDED == decryptVerifyResult.getStatus()) {
+ showPassphraseDialog(decryptVerifyResult.getKeyIdPassphraseNeeded());
+ } else if (PgpDecryptVerifyResult.SYMMETRIC_PASSHRASE_NEEDED ==
+ decryptVerifyResult.getStatus()) {
+ showPassphraseDialog(Id.key.symmetric);
+ } else {
+ AppMsg.makeText(getActivity(), R.string.decryption_successful,
+ AppMsg.STYLE_INFO).show();
+
+ if (mDeleteAfter.isChecked()) {
+ // Create and show dialog to delete original file
+ DeleteFileDialogFragment deleteFileDialog = DeleteFileDialogFragment
+ .newInstance(mInputFilename);
+ deleteFileDialog.show(getActivity().getSupportFragmentManager(), "deleteDialog");
+ }
+
+ OpenPgpSignatureResult signatureResult = decryptVerifyResult.getSignatureResult();
+
+ // display signature result in activity
+ onSignatureResult(signatureResult);
+ }
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // show progress dialog
+ saveHandler.showProgressDialog(getActivity());
+
+ // start service with intent
+ getActivity().startService(intent);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case RESULT_CODE_FILE: {
+ if (resultCode == Activity.RESULT_OK && data != null) {
+ try {
+ String path = FileHelper.getPath(getActivity(), data.getData());
+ Log.d(Constants.TAG, "path=" + path);
+
+ mFilename.setText(path);
+ } catch (NullPointerException e) {
+ Log.e(Constants.TAG, "Nullpointer while retrieving path!");
+ }
+ }
+ return;
+ }
+
+ default: {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ break;
+ }
+ }
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java
new file mode 100644
index 000000000..1c465f55c
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.support.v4.app.Fragment;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.beardedhen.androidbootstrap.BootstrapButton;
+import com.devspark.appmsg.AppMsg;
+
+import org.openintents.openpgp.OpenPgpSignatureResult;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
+import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
+import org.sufficientlysecure.keychain.provider.KeychainContract;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment;
+
+public class DecryptFragment extends Fragment {
+ private static final int RESULT_CODE_LOOKUP_KEY = 0x00007006;
+
+ protected long mSignatureKeyId = 0;
+
+ protected RelativeLayout mSignatureLayout = null;
+ protected ImageView mSignatureStatusImage = null;
+ protected TextView mUserId = null;
+ protected TextView mUserIdRest = null;
+
+ protected BootstrapButton mLookupKey = null;
+
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mSignatureLayout = (RelativeLayout) getView().findViewById(R.id.signature);
+ mSignatureStatusImage = (ImageView) getView().findViewById(R.id.ic_signature_status);
+ mUserId = (TextView) getView().findViewById(R.id.mainUserId);
+ mUserIdRest = (TextView) getView().findViewById(R.id.mainUserIdRest);
+ mLookupKey = (BootstrapButton) getView().findViewById(R.id.lookup_key);
+ mLookupKey.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ lookupUnknownKey(mSignatureKeyId);
+ }
+ });
+ mSignatureLayout.setVisibility(View.GONE);
+ mSignatureLayout.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ lookupUnknownKey(mSignatureKeyId);
+ }
+ });
+ }
+
+ private void lookupUnknownKey(long unknownKeyId) {
+ Intent intent = new Intent(getActivity(), ImportKeysActivity.class);
+ intent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_KEYSERVER);
+ intent.putExtra(ImportKeysActivity.EXTRA_KEY_ID, unknownKeyId);
+ startActivityForResult(intent, RESULT_CODE_LOOKUP_KEY);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+
+ case RESULT_CODE_LOOKUP_KEY: {
+ if (resultCode == Activity.RESULT_OK) {
+ // TODO: generate new OpenPgpSignatureResult and display it
+ }
+ return;
+ }
+
+ default: {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ break;
+ }
+ }
+ }
+
+ protected void onSignatureResult(OpenPgpSignatureResult signatureResult) {
+ mSignatureKeyId = 0;
+ mSignatureLayout.setVisibility(View.GONE);
+ if (signatureResult != null) {
+
+ mSignatureKeyId = signatureResult.getKeyId();
+
+ String userId = signatureResult.getUserId();
+ String[] userIdSplit = PgpKeyHelper.splitUserId(userId);
+ if (userIdSplit[0] != null) {
+ mUserId.setText(userId);
+ } else {
+ mUserId.setText(R.string.user_id_no_name);
+ }
+ if (userIdSplit[1] != null) {
+ mUserIdRest.setText(userIdSplit[1]);
+ } else {
+ mUserIdRest.setText(getString(R.string.label_key_id) + ": "
+ + PgpKeyHelper.convertKeyIdToHex(mSignatureKeyId));
+ }
+
+ switch (signatureResult.getStatus()) {
+ case OpenPgpSignatureResult.SIGNATURE_SUCCESS_UNCERTIFIED: {
+ mSignatureStatusImage.setImageResource(R.drawable.overlay_ok);
+ mLookupKey.setVisibility(View.GONE);
+ break;
+ }
+
+ // TODO!
+// case OpenPgpSignatureResult.SIGNATURE_SUCCESS_CERTIFIED: {
+// break;
+// }
+
+ case OpenPgpSignatureResult.SIGNATURE_UNKNOWN_PUB_KEY: {
+ mSignatureStatusImage.setImageResource(R.drawable.overlay_error);
+ mLookupKey.setVisibility(View.VISIBLE);
+ AppMsg.makeText(getActivity(),
+ R.string.unknown_signature,
+ AppMsg.STYLE_ALERT).show();
+ break;
+ }
+
+ default: {
+ mSignatureStatusImage.setImageResource(R.drawable.overlay_error);
+ mLookupKey.setVisibility(View.GONE);
+ break;
+ }
+ }
+ mSignatureLayout.setVisibility(View.VISIBLE);
+ }
+ }
+
+ protected void showPassphraseDialog(long keyId) {
+ PassphraseDialogFragment.show(getActivity(), keyId,
+ new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) {
+ String passphrase =
+ message.getData().getString(PassphraseDialogFragment.MESSAGE_DATA_PASSPHRASE);
+ decryptStart(passphrase);
+ }
+ }
+ });
+ }
+
+ /**
+ * Should be overridden by MessageFragment and FileFragment to start actual decryption
+ *
+ * @param passphrase
+ */
+ protected void decryptStart(String passphrase) {
+
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptMessageFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptMessageFragment.java
new file mode 100644
index 000000000..2169bbd77
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptMessageFragment.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.app.ProgressDialog;
+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.EditText;
+
+import com.beardedhen.androidbootstrap.BootstrapButton;
+import com.devspark.appmsg.AppMsg;
+
+import org.openintents.openpgp.OpenPgpSignatureResult;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.compatibility.ClipboardReflection;
+import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyResult;
+import org.sufficientlysecure.keychain.pgp.PgpHelper;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.util.regex.Matcher;
+
+public class DecryptMessageFragment extends DecryptFragment {
+ public static final String ARG_CIPHERTEXT = "ciphertext";
+
+ // view
+ private EditText mMessage;
+ private BootstrapButton mDecryptButton;
+ private BootstrapButton mDecryptFromCLipboardButton;
+
+ // model
+ private String mCiphertext;
+
+ /**
+ * Inflate the layout for this fragment
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.decrypt_message_fragment, container, false);
+
+ mMessage = (EditText) view.findViewById(R.id.message);
+ mDecryptButton = (BootstrapButton) view.findViewById(R.id.action_decrypt);
+ mDecryptFromCLipboardButton = (BootstrapButton) view.findViewById(R.id.action_decrypt_from_clipboard);
+ mDecryptButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ decryptClicked();
+ }
+ });
+ mDecryptFromCLipboardButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ decryptFromClipboardClicked();
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ String ciphertext = getArguments().getString(ARG_CIPHERTEXT);
+ if (ciphertext != null) {
+ mMessage.setText(ciphertext);
+ decryptStart(null);
+ }
+ }
+
+ private void decryptClicked() {
+ mCiphertext = mMessage.getText().toString();
+ decryptStart(null);
+ }
+
+ private void decryptFromClipboardClicked() {
+ CharSequence clipboardText = ClipboardReflection.getClipboardText(getActivity());
+
+ // only decrypt if clipboard content is available and a pgp message or cleartext signature
+ if (clipboardText != null) {
+ Matcher matcher = PgpHelper.PGP_MESSAGE.matcher(clipboardText);
+ if (!matcher.matches()) {
+ matcher = PgpHelper.PGP_CLEARTEXT_SIGNATURE.matcher(clipboardText);
+ }
+ if (matcher.matches()) {
+ mCiphertext = matcher.group(1);
+ decryptStart(null);
+ } else {
+ AppMsg.makeText(getActivity(), R.string.error_invalid_data, AppMsg.STYLE_INFO)
+ .show();
+ }
+ } else {
+ AppMsg.makeText(getActivity(), R.string.error_invalid_data, AppMsg.STYLE_INFO)
+ .show();
+ }
+ }
+
+ @Override
+ protected void decryptStart(String passphrase) {
+ Log.d(Constants.TAG, "decryptStart");
+
+ // Send all information needed to service to decrypt in other thread
+ Intent intent = new Intent(getActivity(), KeychainIntentService.class);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ intent.setAction(KeychainIntentService.ACTION_DECRYPT_VERIFY);
+
+ // data
+ data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_BYTES);
+ data.putByteArray(KeychainIntentService.DECRYPT_CIPHERTEXT_BYTES, mCiphertext.getBytes());
+ data.putString(KeychainIntentService.DECRYPT_PASSPHRASE, passphrase);
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after encrypting is done in KeychainIntentService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(getActivity(),
+ getString(R.string.progress_decrypting), ProgressDialog.STYLE_HORIZONTAL) {
+ public void handleMessage(Message message) {
+ // handle messages by standard KeychainIntentServiceHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ // get returned data bundle
+ Bundle returnData = message.getData();
+
+ PgpDecryptVerifyResult decryptVerifyResult =
+ returnData.getParcelable(KeychainIntentService.RESULT_DECRYPT_VERIFY_RESULT);
+
+ if (PgpDecryptVerifyResult.KEY_PASSHRASE_NEEDED == decryptVerifyResult.getStatus()) {
+ showPassphraseDialog(decryptVerifyResult.getKeyIdPassphraseNeeded());
+ } else if (PgpDecryptVerifyResult.SYMMETRIC_PASSHRASE_NEEDED ==
+ decryptVerifyResult.getStatus()) {
+ showPassphraseDialog(Id.key.symmetric);
+ } else {
+ AppMsg.makeText(getActivity(), R.string.decryption_successful,
+ AppMsg.STYLE_INFO).show();
+
+ byte[] decryptedMessage = returnData
+ .getByteArray(KeychainIntentService.RESULT_DECRYPTED_BYTES);
+ mMessage.setText(new String(decryptedMessage));
+ mMessage.setHorizontallyScrolling(false);
+
+ OpenPgpSignatureResult signatureResult = decryptVerifyResult.getSignatureResult();
+
+ // display signature result in activity
+ onSignatureResult(signatureResult);
+ }
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // show progress dialog
+ saveHandler.showProgressDialog(getActivity());
+
+ // start service with intent
+ getActivity().startService(intent);
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DrawerActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DrawerActivity.java
new file mode 100644
index 000000000..c875818e3
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DrawerActivity.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.support.v4.app.ActionBarDrawerToggle;
+import android.support.v4.view.GravityCompat;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.app.ActionBarActivity;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.beardedhen.androidbootstrap.FontAwesomeText;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+
+public class DrawerActivity extends ActionBarActivity {
+ private DrawerLayout mDrawerLayout;
+ private ListView mDrawerList;
+ private ActionBarDrawerToggle mDrawerToggle;
+
+ private CharSequence mDrawerTitle;
+ private CharSequence mTitle;
+ private boolean mIsDrawerLocked = false;
+
+ private Class mSelectedItem;
+
+ private static final int MENU_ID_PREFERENCE = 222;
+ private static final int MENU_ID_HELP = 223;
+
+ protected void setupDrawerNavigation(Bundle savedInstanceState) {
+ mDrawerTitle = getString(R.string.app_name);
+ mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
+ mDrawerList = (ListView) findViewById(R.id.left_drawer);
+ ViewGroup viewGroup = (ViewGroup) findViewById(R.id.content_frame);
+ int leftMarginLoaded = ((ViewGroup.MarginLayoutParams) viewGroup.getLayoutParams()).leftMargin;
+ int leftMarginInTablets = (int) getResources().getDimension(R.dimen.drawer_size);
+ int errorInMarginAllowed = 5;
+
+ // if the left margin of the loaded layout is close to the
+ // one used in tablets then set drawer as open and locked
+ if (Math.abs(leftMarginLoaded - leftMarginInTablets) < errorInMarginAllowed) {
+ mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_OPEN, mDrawerList);
+ mDrawerLayout.setScrimColor(Color.TRANSPARENT);
+ mIsDrawerLocked = true;
+ } else {
+ // set a custom shadow that overlays the main content when the drawer opens
+ mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);
+ mIsDrawerLocked = false;
+ }
+
+ NavItem mItemIconTexts[] = new NavItem[]{
+ new NavItem("fa-user", getString(R.string.nav_contacts)),
+ new NavItem("fa-lock", getString(R.string.nav_encrypt)),
+ new NavItem("fa-unlock", getString(R.string.nav_decrypt)),
+ new NavItem("fa-android", getString(R.string.nav_apps))};
+
+ mDrawerList.setAdapter(new NavigationDrawerAdapter(this, R.layout.drawer_list_item,
+ mItemIconTexts));
+
+ mDrawerList.setOnItemClickListener(new DrawerItemClickListener());
+
+ // enable ActionBar app icon to behave as action to toggle nav drawer
+ // if the drawer is not locked
+ if (!mIsDrawerLocked) {
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setHomeButtonEnabled(true);
+ }
+
+ // ActionBarDrawerToggle ties together the the proper interactions
+ // between the sliding drawer and the action bar app icon
+ mDrawerToggle = new ActionBarDrawerToggle(this, /* host Activity */
+ mDrawerLayout, /* DrawerLayout object */
+ R.drawable.ic_drawer, /* nav drawer image to replace 'Up' caret */
+ R.string.drawer_open, /* "open drawer" description for accessibility */
+ R.string.drawer_close /* "close drawer" description for accessibility */
+ ) {
+ public void onDrawerClosed(View view) {
+ getSupportActionBar().setTitle(mTitle);
+
+ callIntentForDrawerItem(mSelectedItem);
+ }
+
+ public void onDrawerOpened(View drawerView) {
+ mTitle = getSupportActionBar().getTitle();
+ getSupportActionBar().setTitle(mDrawerTitle);
+ // creates call to onPrepareOptionsMenu()
+ supportInvalidateOptionsMenu();
+ }
+ };
+
+ if (!mIsDrawerLocked) {
+ mDrawerLayout.setDrawerListener(mDrawerToggle);
+ } else {
+ // If the drawer is locked open make it un-focusable
+ // so that it doesn't consume all the Back button presses
+ mDrawerLayout.setFocusableInTouchMode(false);
+ }
+ }
+
+ /**
+ * Uses startActivity to call the Intent of the given class
+ *
+ * @param drawerItem the class of the drawer item you want to load. Based on Constants.DrawerItems.*
+ */
+ public void callIntentForDrawerItem(Class drawerItem) {
+ // creates call to onPrepareOptionsMenu()
+ supportInvalidateOptionsMenu();
+
+ // call intent activity if selected
+ if (drawerItem != null) {
+ finish();
+ overridePendingTransition(0, 0);
+
+ Intent intent = new Intent(this, drawerItem);
+ startActivity(intent);
+
+ // disable animation of activity start
+ overridePendingTransition(0, 0);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ if (mDrawerToggle == null) {
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ menu.add(42, MENU_ID_PREFERENCE, 100, R.string.menu_preferences);
+ menu.add(42, MENU_ID_HELP, 101, R.string.menu_help);
+
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (mDrawerToggle == null) {
+ return super.onOptionsItemSelected(item);
+ }
+
+ // The action bar home/up action should open or close the drawer.
+ // ActionBarDrawerToggle will take care of this.
+ if (mDrawerToggle.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ switch (item.getItemId()) {
+ case MENU_ID_PREFERENCE: {
+ Intent intent = new Intent(this, PreferencesActivity.class);
+ startActivity(intent);
+ return true;
+ }
+ case MENU_ID_HELP: {
+ Intent intent = new Intent(this, HelpActivity.class);
+ startActivity(intent);
+ return true;
+ }
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ /**
+ * The click listener for ListView in the navigation drawer
+ */
+ private class DrawerItemClickListener implements ListView.OnItemClickListener {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ selectItem(position);
+ }
+ }
+
+ private void selectItem(int position) {
+ // update selected item and title, then close the drawer
+ mDrawerList.setItemChecked(position, true);
+ // set selected class
+ mSelectedItem = Constants.DrawerItems.ARRAY[position];
+
+ // setTitle(mDrawerTitles[position]);
+ // If drawer isn't locked just close the drawer and
+ // it will move to the selected item by itself (via drawer toggle listener)
+ if (!mIsDrawerLocked) {
+ mDrawerLayout.closeDrawer(mDrawerList);
+ // else move to the selected item yourself
+ } else {
+ callIntentForDrawerItem(mSelectedItem);
+ }
+ }
+
+ /**
+ * When using the ActionBarDrawerToggle, you must call it during onPostCreate() and
+ * onConfigurationChanged()...
+ */
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ // Sync the toggle state after onRestoreInstanceState has occurred.
+ if (mDrawerToggle != null) {
+ mDrawerToggle.syncState();
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ // Pass any configuration change to the drawer toggles
+ if (mDrawerToggle != null) {
+ mDrawerToggle.onConfigurationChanged(newConfig);
+ }
+ }
+
+ private class NavItem {
+ public String icon;
+ public String title;
+
+ public NavItem(String icon, String title) {
+ super();
+ this.icon = icon;
+ this.title = title;
+ }
+ }
+
+ private class NavigationDrawerAdapter extends ArrayAdapter<NavItem> {
+ Context mContext;
+ int mLayoutResourceId;
+ NavItem mData[] = null;
+
+ public NavigationDrawerAdapter(Context context, int layoutResourceId, NavItem[] data) {
+ super(context, layoutResourceId, data);
+ this.mLayoutResourceId = layoutResourceId;
+ this.mContext = context;
+ this.mData = data;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View row = convertView;
+ NavItemHolder holder;
+
+ if (row == null) {
+ LayoutInflater inflater = ((Activity) mContext).getLayoutInflater();
+ row = inflater.inflate(mLayoutResourceId, parent, false);
+
+ holder = new NavItemHolder();
+ holder.mImg = (FontAwesomeText) row.findViewById(R.id.drawer_item_icon);
+ holder.mTxtTitle = (TextView) row.findViewById(R.id.drawer_item_text);
+
+ row.setTag(holder);
+ } else {
+ holder = (NavItemHolder) row.getTag();
+ }
+
+ NavItem item = mData[position];
+ holder.mTxtTitle.setText(item.title);
+ holder.mImg.setIcon(item.icon);
+
+ return row;
+ }
+
+ }
+
+ static class NavItemHolder {
+ FontAwesomeText mImg;
+ TextView mTxtTitle;
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyActivity.java
new file mode 100644
index 000000000..93d5688b9
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyActivity.java
@@ -0,0 +1,760 @@
+/*
+ * Copyright (C) 2012-2014 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 android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+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.support.v4.app.ActivityCompat;
+import android.support.v7.app.ActionBarActivity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.LinearLayout;
+
+import com.beardedhen.androidbootstrap.BootstrapButton;
+import com.devspark.appmsg.AppMsg;
+
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.helper.ActionBarHelper;
+import org.sufficientlysecure.keychain.helper.ExportHelper;
+import org.sufficientlysecure.keychain.pgp.PgpConversionHelper;
+import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
+import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
+import org.sufficientlysecure.keychain.provider.KeychainContract;
+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.service.SaveKeyringParcel;
+import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment;
+import org.sufficientlysecure.keychain.ui.dialog.SetPassphraseDialogFragment;
+import org.sufficientlysecure.keychain.ui.widget.Editor;
+import org.sufficientlysecure.keychain.ui.widget.Editor.EditorListener;
+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 java.util.ArrayList;
+import java.util.GregorianCalendar;
+import java.util.List;
+import java.util.Vector;
+
+public class EditKeyActivity extends ActionBarActivity implements EditorListener {
+
+ // Actions for internal use only:
+ 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 = "user_ids";
+ public static final String EXTRA_NO_PASSPHRASE = "no_passphrase";
+ public static final String EXTRA_GENERATE_DEFAULT_KEYS = "generate_default_keys";
+
+ // EDIT
+ private Uri mDataUri;
+
+ private PGPSecretKeyRing mKeyRing = null;
+
+ private SectionView mUserIdsView;
+ private SectionView mKeysView;
+
+ private String mCurrentPassphrase = null;
+ private String mNewPassphrase = null;
+ private String mSavedNewPassphrase = null;
+ private boolean mIsPassphraseSet;
+ private boolean mNeedsSaving;
+ private boolean mIsBrandNewKeyring = false;
+
+ private BootstrapButton mChangePassphrase;
+
+ private CheckBox mNoPassphrase;
+
+ Vector<String> mUserIds;
+ Vector<PGPSecretKey> mKeys;
+ Vector<Integer> mKeysUsages;
+ boolean mMasterCanSign = true;
+
+ ExportHelper mExportHelper;
+
+ public boolean needsSaving() {
+ mNeedsSaving = (mUserIdsView == null) ? false : mUserIdsView.needsSaving();
+ mNeedsSaving |= (mKeysView == null) ? false : mKeysView.needsSaving();
+ mNeedsSaving |= hasPassphraseChanged();
+ mNeedsSaving |= mIsBrandNewKeyring;
+ return mNeedsSaving;
+ }
+
+
+ public void somethingChanged() {
+ ActivityCompat.invalidateOptionsMenu(this);
+ }
+
+ public void onDeleted(Editor e, boolean wasNewItem) {
+ somethingChanged();
+ }
+
+ public void onEdited() {
+ somethingChanged();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mExportHelper = new ExportHelper(this);
+
+ // Inflate a "Done"/"Cancel" custom action bar view
+ ActionBarHelper.setTwoButtonView(getSupportActionBar(),
+ R.string.btn_save, R.drawable.ic_action_save,
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // Save
+ saveClicked();
+ }
+ }, R.string.menu_key_edit_cancel, R.drawable.ic_action_cancel,
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // Cancel
+ cancelClicked();
+ }
+ }
+ );
+
+ 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);
+ }
+ }
+
+ /**
+ * Handle intent action to create new key
+ *
+ * @param intent
+ */
+ private void handleActionCreateKey(Intent intent) {
+ Bundle extras = intent.getExtras();
+
+ mCurrentPassphrase = "";
+ mIsBrandNewKeyring = true;
+
+ 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) {
+
+ // Send all information needed to service generate keys in other thread
+ final Intent serviceIntent = new Intent(this, KeychainIntentService.class);
+ serviceIntent.setAction(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 KeychainIntentService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(
+ this, getResources().getQuantityString(R.plurals.progress_generating, 1),
+ ProgressDialog.STYLE_HORIZONTAL, true,
+
+ new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ // Stop key generation on cancel
+ stopService(serviceIntent);
+ EditKeyActivity.this.setResult(Activity.RESULT_CANCELED);
+ EditKeyActivity.this.finish();
+ }
+ }) {
+
+ @Override
+ public void handleMessage(Message message) {
+ // handle messages by standard KeychainIntentServiceHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ // get new key from data bundle returned from service
+ Bundle data = message.getData();
+
+ ArrayList<PGPSecretKey> newKeys =
+ PgpConversionHelper.BytesToPGPSecretKeyList(data
+ .getByteArray(KeychainIntentService.RESULT_NEW_KEY));
+
+ ArrayList<Integer> keyUsageFlags = data.getIntegerArrayList(
+ KeychainIntentService.RESULT_KEY_USAGES);
+
+ if (newKeys.size() == keyUsageFlags.size()) {
+ for (int i = 0; i < newKeys.size(); ++i) {
+ mKeys.add(newKeys.get(i));
+ mKeysUsages.add(keyUsageFlags.get(i));
+ }
+ }
+
+ buildLayout(true);
+ }
+ }
+ };
+
+ // 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);
+ }
+ }
+ } else {
+ buildLayout(false);
+ }
+ }
+
+ /**
+ * Handle intent action to edit existing key
+ *
+ * @param intent
+ */
+ private void handleActionEditKey(Intent intent) {
+ mDataUri = intent.getData();
+ if (mDataUri == null) {
+ Log.e(Constants.TAG, "Intent data missing. Should be Uri of key!");
+ finish();
+ } else {
+ Log.d(Constants.TAG, "uri: " + mDataUri);
+
+ // get master key id using row id
+ long masterKeyId = ProviderHelper.getMasterKeyId(this, mDataUri);
+ finallyEdit(masterKeyId);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private void finallyEdit(final long masterKeyId) {
+ if (masterKeyId != 0) {
+ PGPSecretKey masterKey = null;
+ mKeyRing = ProviderHelper.getPGPSecretKeyRing(this, masterKeyId);
+ if (mKeyRing != null) {
+ masterKey = mKeyRing.getSecretKey();
+ mMasterCanSign = PgpKeyHelper.isCertificationKey(mKeyRing.getSecretKey());
+ for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(mKeyRing.getSecretKeys())) {
+ mKeys.add(key);
+ mKeysUsages.add(-1); // get usage when view is created
+ }
+ } else {
+ Log.e(Constants.TAG, "Keyring not found with masterKeyId: " + masterKeyId);
+ AppMsg.makeText(this, R.string.error_no_secret_key_found, AppMsg.STYLE_ALERT).show();
+ // TODO
+ }
+ if (masterKey != null) {
+ boolean isSet = false;
+ for (String userId : new IterableIterator<String>(masterKey.getUserIDs())) {
+ Log.d(Constants.TAG, "Added userId " + userId);
+ if (!isSet) {
+ isSet = true;
+ String[] parts = PgpKeyHelper.splitUserId(userId);
+ if (parts[0] != null) {
+ setTitle(parts[0]);
+ }
+ }
+ mUserIds.add(userId);
+ }
+ }
+ }
+
+ mCurrentPassphrase = "";
+ buildLayout(false);
+
+ mIsPassphraseSet = PassphraseCacheService.hasPassphrase(this, masterKeyId);
+ if (!mIsPassphraseSet) {
+ // check "no passphrase" checkbox and remove button
+ mNoPassphrase.setChecked(true);
+ mChangePassphrase.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * 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();
+ somethingChanged();
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(returnHandler);
+
+ // set title based on isPassphraseSet()
+ int title;
+ if (isPassphraseSet()) {
+ title = R.string.title_change_passphrase;
+ } else {
+ title = R.string.title_set_passphrase;
+ }
+
+ 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.
+ *
+ * @param newKeys
+ */
+ private void buildLayout(boolean newKeys) {
+ setContentView(R.layout.edit_key_activity);
+
+ // find views
+ mChangePassphrase = (BootstrapButton) findViewById(R.id.edit_key_btn_change_passphrase);
+ mNoPassphrase = (CheckBox) findViewById(R.id.edit_key_no_passphrase);
+ // 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);
+ if (mIsPassphraseSet) {
+ mChangePassphrase.setText(getString(R.string.btn_change_passphrase));
+ }
+ mUserIdsView = (SectionView) inflater.inflate(R.layout.edit_key_section, container, false);
+ mUserIdsView.setType(Id.type.user_id);
+ mUserIdsView.setCanBeEdited(mMasterCanSign);
+ mUserIdsView.setUserIds(mUserIds);
+ mUserIdsView.setEditorListener(this);
+ container.addView(mUserIdsView);
+ mKeysView = (SectionView) inflater.inflate(R.layout.edit_key_section, container, false);
+ mKeysView.setType(Id.type.key);
+ mKeysView.setCanBeEdited(mMasterCanSign);
+ mKeysView.setKeys(mKeys, mKeysUsages, newKeys);
+ mKeysView.setEditorListener(this);
+ container.addView(mKeysView);
+
+ updatePassphraseButtonText();
+
+ mChangePassphrase.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ showSetPassphraseDialog();
+ }
+ });
+
+ // disable passphrase when no passphrase checkbox is checked!
+ mNoPassphrase.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ if (isChecked) {
+ // remove passphrase
+ mSavedNewPassphrase = mNewPassphrase;
+ mNewPassphrase = "";
+ mChangePassphrase.setVisibility(View.GONE);
+ } else {
+ mNewPassphrase = mSavedNewPassphrase;
+ mChangePassphrase.setVisibility(View.VISIBLE);
+ }
+ somethingChanged();
+ }
+ });
+ }
+
+ 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 ((mIsPassphraseSet)
+ || (mNewPassphrase != null && !mNewPassphrase.equals(""))) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public boolean hasPassphraseChanged() {
+ if (mNoPassphrase != null) {
+ if (mNoPassphrase.isChecked()) {
+ return mIsPassphraseSet;
+ } else {
+ return (mNewPassphrase != null && !mNewPassphrase.equals(""));
+ }
+ } else {
+ return false;
+ }
+ }
+
+ private void saveClicked() {
+ final long masterKeyId = getMasterKeyId();
+ if (needsSaving()) { //make sure, as some versions don't support invalidateOptionsMenu
+ try {
+ if (!isPassphraseSet()) {
+ throw new PgpGeneralException(this.getString(R.string.set_a_passphrase));
+ }
+
+ String passphrase;
+ if (mIsPassphraseSet) {
+ passphrase = PassphraseCacheService.getCachedPassphrase(this, masterKeyId);
+ } else {
+ passphrase = "";
+ }
+ if (passphrase == null) {
+ PassphraseDialogFragment.show(this, masterKeyId,
+ new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) {
+ mCurrentPassphrase = PassphraseCacheService.getCachedPassphrase(
+ EditKeyActivity.this, masterKeyId);
+ checkEmptyIDsWanted();
+ }
+ }
+ });
+ } else {
+ mCurrentPassphrase = passphrase;
+ checkEmptyIDsWanted();
+ }
+ } catch (PgpGeneralException e) {
+ AppMsg.makeText(this, getString(R.string.error_message, e.getMessage()),
+ AppMsg.STYLE_ALERT).show();
+ }
+ } else {
+ AppMsg.makeText(this, R.string.error_change_something_first, AppMsg.STYLE_ALERT).show();
+ }
+ }
+
+ private void checkEmptyIDsWanted() {
+ try {
+ ArrayList<String> userIDs = getUserIds(mUserIdsView);
+ List<Boolean> newIDs = mUserIdsView.getNewIDFlags();
+ ArrayList<String> originalIDs = mUserIdsView.getOriginalIDs();
+ int curID = 0;
+ for (String userID : userIDs) {
+ if (userID.equals("") && (!userID.equals(originalIDs.get(curID)) || newIDs.get(curID))) {
+ AlertDialog.Builder alert = new AlertDialog.Builder(
+ EditKeyActivity.this);
+
+ alert.setIcon(R.drawable.ic_dialog_alert_holo_light);
+ alert.setTitle(R.string.warning);
+ alert.setMessage(EditKeyActivity.this.getString(R.string.ask_empty_id_ok));
+
+ alert.setPositiveButton(EditKeyActivity.this.getString(android.R.string.yes),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.dismiss();
+ finallySaveClicked();
+ }
+ }
+ );
+ alert.setNegativeButton(this.getString(android.R.string.no),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.dismiss();
+ }
+ }
+ );
+ alert.setCancelable(false);
+ alert.create().show();
+ return;
+ }
+ curID++;
+ }
+ } catch (PgpGeneralException e) {
+ Log.e(Constants.TAG, getString(R.string.error_message, e.getMessage()));
+ AppMsg.makeText(this, getString(R.string.error_message, e.getMessage()), AppMsg.STYLE_ALERT).show();
+ }
+ finallySaveClicked();
+ }
+
+ private boolean[] toPrimitiveArray(final List<Boolean> booleanList) {
+ final boolean[] primitives = new boolean[booleanList.size()];
+ int index = 0;
+ for (Boolean object : booleanList) {
+ primitives[index++] = object;
+ }
+ return primitives;
+ }
+
+ private void finallySaveClicked() {
+ try {
+ // Send all information needed to service to edit key in other thread
+ Intent intent = new Intent(this, KeychainIntentService.class);
+
+ intent.setAction(KeychainIntentService.ACTION_SAVE_KEYRING);
+
+ SaveKeyringParcel saveParams = new SaveKeyringParcel();
+ saveParams.userIDs = getUserIds(mUserIdsView);
+ saveParams.originalIDs = mUserIdsView.getOriginalIDs();
+ saveParams.deletedIDs = mUserIdsView.getDeletedIDs();
+ saveParams.newIDs = toPrimitiveArray(mUserIdsView.getNewIDFlags());
+ saveParams.primaryIDChanged = mUserIdsView.primaryChanged();
+ saveParams.moddedKeys = toPrimitiveArray(mKeysView.getNeedsSavingArray());
+ saveParams.deletedKeys = mKeysView.getDeletedKeys();
+ saveParams.keysExpiryDates = getKeysExpiryDates(mKeysView);
+ saveParams.keysUsages = getKeysUsages(mKeysView);
+ saveParams.newPassphrase = mNewPassphrase;
+ saveParams.oldPassphrase = mCurrentPassphrase;
+ saveParams.newKeys = toPrimitiveArray(mKeysView.getNewKeysArray());
+ saveParams.keys = getKeys(mKeysView);
+ saveParams.originalPrimaryID = mUserIdsView.getOriginalPrimaryID();
+
+
+ // fill values for this action
+ Bundle data = new Bundle();
+ data.putBoolean(KeychainIntentService.SAVE_KEYRING_CAN_SIGN, mMasterCanSign);
+ data.putParcelable(KeychainIntentService.SAVE_KEYRING_PARCEL, saveParams);
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after saving is done in KeychainIntentService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
+ getString(R.string.progress_saving), ProgressDialog.STYLE_HORIZONTAL) {
+ public void handleMessage(Message message) {
+ // handle messages by standard KeychainIntentServiceHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ Intent data = new Intent();
+
+ // return uri pointing to new created key
+ Uri uri = KeychainContract.KeyRings.buildGenericKeyRingUri(
+ String.valueOf(getMasterKeyId()));
+ data.setData(uri);
+
+ 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 (PgpGeneralException e) {
+ Log.e(Constants.TAG, getString(R.string.error_message, e.getMessage()));
+ AppMsg.makeText(this, getString(R.string.error_message, e.getMessage()),
+ AppMsg.STYLE_ALERT).show();
+ }
+ }
+
+ private void cancelClicked() {
+ if (needsSaving()) { //ask if we want to save
+ AlertDialog.Builder alert = new AlertDialog.Builder(
+ EditKeyActivity.this);
+
+ alert.setIcon(R.drawable.ic_dialog_alert_holo_light);
+ alert.setTitle(R.string.warning);
+ alert.setMessage(EditKeyActivity.this.getString(R.string.ask_save_changed_key));
+
+ alert.setPositiveButton(EditKeyActivity.this.getString(android.R.string.yes),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.dismiss();
+ saveClicked();
+ }
+ });
+ alert.setNegativeButton(this.getString(android.R.string.no),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.dismiss();
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ });
+ alert.setCancelable(false);
+ alert.create().show();
+ } else {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ }
+
+ /**
+ * Returns user ids from the SectionView
+ *
+ * @param userIdsView
+ * @return
+ */
+ private ArrayList<String> getUserIds(SectionView userIdsView) throws 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;
+ userId = editor.getValue();
+
+ if (editor.isMainUserId()) {
+ userIds.add(0, userId);
+ gotMainUserId = true;
+ } else {
+ userIds.add(userId);
+ }
+ }
+
+ if (userIds.size() == 0) {
+ throw new PgpGeneralException(getString(R.string.error_key_needs_a_user_id));
+ }
+
+ if (!gotMainUserId) {
+ throw new PgpGeneralException(getString(R.string.error_main_user_id_must_not_be_empty));
+ }
+
+ return userIds;
+ }
+
+ /**
+ * Returns keys from the SectionView
+ *
+ * @param keysView
+ * @return
+ */
+ private ArrayList<PGPSecretKey> getKeys(SectionView keysView) throws PgpGeneralException {
+ ArrayList<PGPSecretKey> keys = new ArrayList<PGPSecretKey>();
+
+ ViewGroup keyEditors = keysView.getEditors();
+
+ if (keyEditors.getChildCount() == 0) {
+ throw new PgpGeneralException(getString(R.string.error_key_needs_master_key));
+ }
+
+ 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 PgpGeneralException {
+ ArrayList<Integer> keysUsages = new ArrayList<Integer>();
+
+ ViewGroup keyEditors = keysView.getEditors();
+
+ if (keyEditors.getChildCount() == 0) {
+ throw new PgpGeneralException(getString(R.string.error_key_needs_master_key));
+ }
+
+ for (int i = 0; i < keyEditors.getChildCount(); ++i) {
+ KeyEditor editor = (KeyEditor) keyEditors.getChildAt(i);
+ keysUsages.add(editor.getUsage());
+ }
+
+ return keysUsages;
+ }
+
+ private ArrayList<GregorianCalendar> getKeysExpiryDates(SectionView keysView) throws PgpGeneralException {
+ ArrayList<GregorianCalendar> keysExpiryDates = new ArrayList<GregorianCalendar>();
+
+ ViewGroup keyEditors = keysView.getEditors();
+
+ if (keyEditors.getChildCount() == 0) {
+ throw new PgpGeneralException(getString(R.string.error_key_needs_master_key));
+ }
+
+ for (int i = 0; i < keyEditors.getChildCount(); ++i) {
+ KeyEditor editor = (KeyEditor) keyEditors.getChildAt(i);
+ keysExpiryDates.add(editor.getExpiryDate());
+ }
+
+ return keysExpiryDates;
+ }
+
+ private void updatePassphraseButtonText() {
+ mChangePassphrase.setText(isPassphraseSet() ? getString(R.string.btn_change_passphrase)
+ : getString(R.string.btn_set_passphrase));
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptActivity.java
new file mode 100644
index 000000000..a03c7d797
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptActivity.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2012-2014 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 android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.view.PagerTabStrip;
+import android.support.v4.view.ViewPager;
+import android.widget.Toast;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.helper.ActionBarHelper;
+import org.sufficientlysecure.keychain.helper.FileHelper;
+import org.sufficientlysecure.keychain.ui.adapter.PagerTabStripAdapter;
+import org.sufficientlysecure.keychain.util.Log;
+
+public class EncryptActivity extends DrawerActivity implements
+ EncryptSymmetricFragment.OnSymmetricKeySelection,
+ EncryptAsymmetricFragment.OnAsymmetricKeySelection,
+ EncryptActivityInterface {
+
+ /* Intents */
+ public static final String ACTION_ENCRYPT = Constants.INTENT_PREFIX + "ENCRYPT";
+
+ /* EXTRA keys for input */
+ public static final String EXTRA_TEXT = "text";
+
+ // enables ASCII Armor for file encryption when uri is given
+ public static final String EXTRA_ASCII_ARMOR = "ascii_armor";
+
+ // preselect ids, for internal use
+ public static final String EXTRA_SIGNATURE_KEY_ID = "signature_key_id";
+ public static final String EXTRA_ENCRYPTION_KEY_IDS = "encryption_key_ids";
+
+ // view
+ ViewPager mViewPagerMode;
+ PagerTabStrip mPagerTabStripMode;
+ PagerTabStripAdapter mTabsAdapterMode;
+ ViewPager mViewPagerContent;
+ PagerTabStrip mPagerTabStripContent;
+ PagerTabStripAdapter mTabsAdapterContent;
+
+ // tabs
+ Bundle mAsymmetricFragmentBundle = new Bundle();
+ Bundle mSymmetricFragmentBundle = new Bundle();
+ Bundle mMessageFragmentBundle = new Bundle();
+ Bundle mFileFragmentBundle = new Bundle();
+ int mSwitchToMode = PAGER_MODE_ASYMMETRIC;
+ int mSwitchToContent = PAGER_CONTENT_MESSAGE;
+
+ private static final int PAGER_MODE_ASYMMETRIC = 0;
+ private static final int PAGER_MODE_SYMMETRIC = 1;
+ private static final int PAGER_CONTENT_MESSAGE = 0;
+ private static final int PAGER_CONTENT_FILE = 1;
+
+ // model useb by message and file fragment
+ private long mEncryptionKeyIds[] = null;
+ private long mSigningKeyId = Id.key.none;
+ private String mPassphrase;
+ private String mPassphraseAgain;
+
+ @Override
+ public void onSigningKeySelected(long signingKeyId) {
+ mSigningKeyId = signingKeyId;
+ }
+
+ @Override
+ public void onEncryptionKeysSelected(long[] encryptionKeyIds) {
+ mEncryptionKeyIds = encryptionKeyIds;
+ }
+
+ @Override
+ public void onPassphraseUpdate(String passphrase) {
+ mPassphrase = passphrase;
+ }
+
+ @Override
+ public void onPassphraseAgainUpdate(String passphrase) {
+ mPassphraseAgain = passphrase;
+ }
+
+ @Override
+ public boolean isModeSymmetric() {
+ if (PAGER_MODE_SYMMETRIC == mViewPagerMode.getCurrentItem()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public long getSignatureKey() {
+ return mSigningKeyId;
+ }
+
+ @Override
+ public long[] getEncryptionKeys() {
+ return mEncryptionKeyIds;
+ }
+
+ @Override
+ public String getPassphrase() {
+ return mPassphrase;
+ }
+
+ @Override
+ public String getPassphraseAgain() {
+ return mPassphraseAgain;
+ }
+
+
+ private void initView() {
+ mViewPagerMode = (ViewPager) findViewById(R.id.encrypt_pager_mode);
+ mPagerTabStripMode = (PagerTabStrip) findViewById(R.id.encrypt_pager_tab_strip_mode);
+ mViewPagerContent = (ViewPager) findViewById(R.id.encrypt_pager_content);
+ mPagerTabStripContent = (PagerTabStrip) findViewById(R.id.encrypt_pager_tab_strip_content);
+
+ mTabsAdapterMode = new PagerTabStripAdapter(this);
+ mViewPagerMode.setAdapter(mTabsAdapterMode);
+ mTabsAdapterContent = new PagerTabStripAdapter(this);
+ mViewPagerContent.setAdapter(mTabsAdapterContent);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.encrypt_activity);
+
+ // set actionbar without home button if called from another app
+ ActionBarHelper.setBackButton(this);
+
+ initView();
+
+ setupDrawerNavigation(savedInstanceState);
+
+ // Handle intent actions
+ handleActions(getIntent());
+
+ mTabsAdapterMode.addTab(EncryptAsymmetricFragment.class,
+ mAsymmetricFragmentBundle, getString(R.string.label_asymmetric));
+ mTabsAdapterMode.addTab(EncryptSymmetricFragment.class,
+ mSymmetricFragmentBundle, getString(R.string.label_symmetric));
+ mViewPagerMode.setCurrentItem(mSwitchToMode);
+
+ mTabsAdapterContent.addTab(EncryptMessageFragment.class,
+ mMessageFragmentBundle, getString(R.string.label_message));
+ mTabsAdapterContent.addTab(EncryptFileFragment.class,
+ mFileFragmentBundle, getString(R.string.label_file));
+ mViewPagerContent.setCurrentItem(mSwitchToContent);
+ }
+
+ /**
+ * 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
+ // executeServiceMethod ACTION_ENCRYPT in main actions
+ extras.putString(EXTRA_TEXT, sharedText);
+ extras.putBoolean(EXTRA_ASCII_ARMOR, true);
+ action = ACTION_ENCRYPT;
+ }
+ } else {
+ // Files via content provider, override uri and action
+ uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
+ action = ACTION_ENCRYPT;
+ }
+ }
+
+ if (extras.containsKey(EXTRA_ASCII_ARMOR)) {
+ boolean requestAsciiArmor = extras.getBoolean(EXTRA_ASCII_ARMOR, true);
+ mFileFragmentBundle.putBoolean(EncryptFileFragment.ARG_ASCII_ARMOR, requestAsciiArmor);
+ }
+
+ String textData = extras.getString(EXTRA_TEXT);
+
+ long signatureKeyId = extras.getLong(EXTRA_SIGNATURE_KEY_ID);
+ long[] encryptionKeyIds = extras.getLongArray(EXTRA_ENCRYPTION_KEY_IDS);
+
+ // preselect keys given by intent
+ mAsymmetricFragmentBundle.putLongArray(EncryptAsymmetricFragment.ARG_ENCRYPTION_KEY_IDS,
+ encryptionKeyIds);
+ mAsymmetricFragmentBundle.putLong(EncryptAsymmetricFragment.ARG_SIGNATURE_KEY_ID,
+ signatureKeyId);
+ mSwitchToMode = PAGER_MODE_ASYMMETRIC;
+
+ /**
+ * Main Actions
+ */
+ if (ACTION_ENCRYPT.equals(action) && textData != null) {
+ // encrypt text based on given extra
+ mMessageFragmentBundle.putString(EncryptMessageFragment.ARG_TEXT, textData);
+ mSwitchToContent = PAGER_CONTENT_MESSAGE;
+ } else if (ACTION_ENCRYPT.equals(action) && uri != null) {
+ // encrypt file based on Uri
+
+ // get file path from uri
+ String path = FileHelper.getPath(this, uri);
+
+ if (path != null) {
+ mFileFragmentBundle.putString(EncryptFileFragment.ARG_FILENAME, path);
+ mSwitchToContent = PAGER_CONTENT_FILE;
+ } else {
+ Log.e(Constants.TAG,
+ "Direct binary data without actual file in filesystem is not supported " +
+ "by Intents. Please use the Remote Service API!");
+ Toast.makeText(this, R.string.error_only_files_are_supported, Toast.LENGTH_LONG)
+ .show();
+ // end activity
+ finish();
+ }
+ } else {
+ Log.e(Constants.TAG,
+ "Include the extra 'text' or an Uri with setData() in your Intent!");
+ }
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptActivityInterface.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptActivityInterface.java
new file mode 100644
index 000000000..0786b3a16
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptActivityInterface.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+public interface EncryptActivityInterface {
+
+ public boolean isModeSymmetric();
+
+ public long getSignatureKey();
+ public long[] getEncryptionKeys();
+
+ public String getPassphrase();
+ public String getPassphraseAgain();
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptAsymmetricFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptAsymmetricFragment.java
new file mode 100644
index 000000000..8400cf397
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptAsymmetricFragment.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.TextView;
+
+import com.beardedhen.androidbootstrap.BootstrapButton;
+
+import org.spongycastle.openpgp.PGPPublicKey;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+
+import java.util.HashMap;
+import java.util.Vector;
+
+public class EncryptAsymmetricFragment extends Fragment {
+ public static final String ARG_SIGNATURE_KEY_ID = "signature_key_id";
+ public static final String ARG_ENCRYPTION_KEY_IDS = "encryption_key_ids";
+
+ public static final int RESULT_CODE_PUBLIC_KEYS = 0x00007001;
+ public static final int RESULT_CODE_SECRET_KEYS = 0x00007002;
+
+ OnAsymmetricKeySelection mKeySelectionListener;
+
+ // view
+ private BootstrapButton mSelectKeysButton;
+ private CheckBox mSign;
+ private TextView mMainUserId;
+ private TextView mMainUserIdRest;
+
+ // model
+ private long mSecretKeyId = Id.key.none;
+ private long mEncryptionKeyIds[] = null;
+
+ // Container Activity must implement this interface
+ public interface OnAsymmetricKeySelection {
+ public void onSigningKeySelected(long signingKeyId);
+
+ public void onEncryptionKeysSelected(long[] encryptionKeyIds);
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ try {
+ mKeySelectionListener = (OnAsymmetricKeySelection) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString() + " must implement OnAsymmetricKeySelection");
+ }
+ }
+
+ private void setSignatureKeyId(long signatureKeyId) {
+ mSecretKeyId = signatureKeyId;
+ // update key selection in EncryptActivity
+ mKeySelectionListener.onSigningKeySelected(signatureKeyId);
+ updateView();
+ }
+
+ private void setEncryptionKeyIds(long[] encryptionKeyIds) {
+ mEncryptionKeyIds = encryptionKeyIds;
+ // update key selection in EncryptActivity
+ mKeySelectionListener.onEncryptionKeysSelected(encryptionKeyIds);
+ updateView();
+ }
+
+ /**
+ * Inflate the layout for this fragment
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.encrypt_asymmetric_fragment, container, false);
+
+ mSelectKeysButton = (BootstrapButton) view.findViewById(R.id.btn_selectEncryptKeys);
+ mSign = (CheckBox) view.findViewById(R.id.sign);
+ mMainUserId = (TextView) view.findViewById(R.id.mainUserId);
+ mMainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest);
+ mSelectKeysButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ selectPublicKeys();
+ }
+ });
+ mSign.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ CheckBox checkBox = (CheckBox) v;
+ if (checkBox.isChecked()) {
+ selectSecretKey();
+ } else {
+ setSignatureKeyId(Id.key.none);
+ }
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ long signatureKeyId = getArguments().getLong(ARG_SIGNATURE_KEY_ID);
+ long[] encryptionKeyIds = getArguments().getLongArray(ARG_ENCRYPTION_KEY_IDS);
+
+ // preselect keys given by arguments (given by Intent to EncryptActivity)
+ preselectKeys(signatureKeyId, encryptionKeyIds);
+ }
+
+ /**
+ * 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) {
+ // TODO: don't use bouncy castle objects!
+ PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingWithKeyId(getActivity(),
+ preselectedSignatureKeyId);
+ PGPSecretKey masterKey;
+ if (keyRing != null) {
+ masterKey = keyRing.getSecretKey();
+ if (masterKey != null) {
+ Vector<PGPSecretKey> signKeys = PgpKeyHelper.getUsableSigningKeys(keyRing);
+ if (signKeys.size() > 0) {
+ setSignatureKeyId(masterKey.getKeyID());
+ }
+ }
+ }
+ }
+
+ if (preselectedEncryptionKeyIds != null) {
+ Vector<Long> goodIds = new Vector<Long>();
+ for (int i = 0; i < preselectedEncryptionKeyIds.length; ++i) {
+ long id = ProviderHelper.getMasterKeyId(getActivity(),
+ KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(Long.toString(preselectedEncryptionKeyIds[i]))
+ );
+ // TODO check for available encrypt keys... is this even relevant?
+ goodIds.add(id);
+ }
+ if (goodIds.size() > 0) {
+ long[] keyIds = new long[goodIds.size()];
+ for (int i = 0; i < goodIds.size(); ++i) {
+ keyIds[i] = goodIds.get(i);
+ }
+ setEncryptionKeyIds(keyIds);
+ }
+ }
+ }
+
+ private void updateView() {
+ if (mEncryptionKeyIds == null || mEncryptionKeyIds.length == 0) {
+ mSelectKeysButton.setText(getString(R.string.select_keys_button_default));
+ } else {
+ mSelectKeysButton.setText(getResources().getQuantityString(
+ R.plurals.select_keys_button, mEncryptionKeyIds.length,
+ mEncryptionKeyIds.length));
+ }
+
+ if (mSecretKeyId == Id.key.none) {
+ mSign.setChecked(false);
+ mMainUserId.setText("");
+ mMainUserIdRest.setText("");
+ } else {
+ // See if we can get a user_id from a unified query
+ String userIdResult = (String) ProviderHelper.getUnifiedData(
+ getActivity(), mSecretKeyId, KeyRings.USER_ID, ProviderHelper.FIELD_TYPE_STRING);
+ String[] userId = PgpKeyHelper.splitUserId(userIdResult);
+ if (userId[0] != null) {
+ mMainUserId.setText(userId[0]);
+ } else {
+ mMainUserId.setText(getResources().getString(R.string.user_id_no_name));
+ }
+ if (userId[1] != null) {
+ mMainUserIdRest.setText(userId[1]);
+ } else {
+ mMainUserIdRest.setText("");
+ }
+ mSign.setChecked(true);
+ }
+ }
+
+ private void selectPublicKeys() {
+ Intent intent = new Intent(getActivity(), 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(getActivity(), SelectSecretKeyActivity.class);
+ startActivityForResult(intent, Id.request.secret_keys);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case RESULT_CODE_PUBLIC_KEYS: {
+ if (resultCode == Activity.RESULT_OK) {
+ Bundle bundle = data.getExtras();
+ setEncryptionKeyIds(bundle
+ .getLongArray(SelectPublicKeyActivity.RESULT_EXTRA_MASTER_KEY_IDS));
+ }
+ break;
+ }
+
+ case RESULT_CODE_SECRET_KEYS: {
+ if (resultCode == Activity.RESULT_OK) {
+ Uri uriMasterKey = data.getData();
+ setSignatureKeyId(Long.valueOf(uriMasterKey.getLastPathSegment()));
+ } else {
+ setSignatureKeyId(Id.key.none);
+ }
+ break;
+ }
+
+ default: {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ break;
+ }
+ }
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFileFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFileFragment.java
new file mode 100644
index 000000000..470c85715
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFileFragment.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.app.Activity;
+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.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.Spinner;
+
+import com.beardedhen.androidbootstrap.BootstrapButton;
+import com.devspark.appmsg.AppMsg;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.helper.FileHelper;
+import org.sufficientlysecure.keychain.helper.Preferences;
+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 java.io.File;
+
+public class EncryptFileFragment extends Fragment {
+ public static final String ARG_FILENAME = "filename";
+ public static final String ARG_ASCII_ARMOR = "ascii_armor";
+
+ private static final int RESULT_CODE_FILE = 0x00007003;
+
+ private EncryptActivityInterface mEncryptInterface;
+
+ // view
+ private CheckBox mAsciiArmor = null;
+ private Spinner mFileCompression = null;
+ private EditText mFilename = null;
+ private CheckBox mDeleteAfter = null;
+ private CheckBox mShareAfter = null;
+ private BootstrapButton mBrowse = null;
+ private BootstrapButton mEncryptFile;
+
+ private FileDialogFragment mFileDialog;
+
+ // model
+ private String mInputFilename = null;
+ private String mOutputFilename = null;
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ try {
+ mEncryptInterface = (EncryptActivityInterface) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString() + " must implement EncryptActivityInterface");
+ }
+ }
+
+ /**
+ * Inflate the layout for this fragment
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.encrypt_file_fragment, container, false);
+
+ mEncryptFile = (BootstrapButton) view.findViewById(R.id.action_encrypt_file);
+ mEncryptFile.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ encryptClicked();
+ }
+ });
+
+ mFilename = (EditText) view.findViewById(R.id.filename);
+ mBrowse = (BootstrapButton) view.findViewById(R.id.btn_browse);
+ mBrowse.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ FileHelper.openFile(EncryptFileFragment.this, mFilename.getText().toString(), "*/*",
+ Id.request.filename);
+ }
+ });
+
+ mFileCompression = (Spinner) view.findViewById(R.id.fileCompression);
+ Choice[] choices = new Choice[] {
+ new Choice(Id.choice.compression.none, getString(R.string.choice_none) + " ("
+ + getString(R.string.compression_fast) + ")"),
+ new Choice(Id.choice.compression.zip, "ZIP ("
+ + getString(R.string.compression_fast) + ")"),
+ new Choice(Id.choice.compression.zlib, "ZLIB ("
+ + getString(R.string.compression_fast) + ")"),
+ new Choice(Id.choice.compression.bzip2, "BZIP2 ("
+ + getString(R.string.compression_very_slow) + ")"),
+ };
+ ArrayAdapter<Choice> adapter = new ArrayAdapter<Choice>(getActivity(),
+ android.R.layout.simple_spinner_item, choices);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mFileCompression.setAdapter(adapter);
+
+ int defaultFileCompression = Preferences.getPreferences(getActivity()).getDefaultFileCompression();
+ for (int i = 0; i < choices.length; ++i) {
+ if (choices[i].getId() == defaultFileCompression) {
+ mFileCompression.setSelection(i);
+ break;
+ }
+ }
+
+ mDeleteAfter = (CheckBox) view.findViewById(R.id.deleteAfterEncryption);
+ mShareAfter = (CheckBox) view.findViewById(R.id.shareAfterEncryption);
+
+ mAsciiArmor = (CheckBox) view.findViewById(R.id.asciiArmor);
+ mAsciiArmor.setChecked(Preferences.getPreferences(getActivity()).getDefaultAsciiArmor());
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ String filename = getArguments().getString(ARG_FILENAME);
+ if (filename != null) {
+ mFilename.setText(filename);
+ }
+ boolean asciiArmor = getArguments().getBoolean(ARG_ASCII_ARMOR);
+ if (asciiArmor) {
+ mAsciiArmor.setChecked(asciiArmor);
+ }
+ }
+
+ /**
+ * 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 = (mAsciiArmor.isChecked() ? ".asc" : ".gpg");
+ String outputFilename = file.getParent() + File.separator + file.getName() + ending;
+
+ return outputFilename;
+ }
+
+ 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_encrypt_to_file),
+ getString(R.string.specify_file_to_encrypt_to), mOutputFilename, null);
+
+ mFileDialog.show(getActivity().getSupportFragmentManager(), "fileDialog");
+ }
+
+ private void encryptClicked() {
+ String currentFilename = mFilename.getText().toString();
+ if (mInputFilename == null || !mInputFilename.equals(currentFilename)) {
+ mInputFilename = mFilename.getText().toString();
+ }
+
+ mOutputFilename = guessOutputFilename(mInputFilename);
+
+ if (mInputFilename.equals("")) {
+ AppMsg.makeText(getActivity(), R.string.no_file_selected, AppMsg.STYLE_ALERT).show();
+ return;
+ }
+
+ if (!mInputFilename.startsWith("content")) {
+ File file = new File(mInputFilename);
+ if (!file.exists() || !file.isFile()) {
+ AppMsg.makeText(
+ getActivity(),
+ getString(R.string.error_message,
+ getString(R.string.error_file_not_found)), AppMsg.STYLE_ALERT)
+ .show();
+ return;
+ }
+ }
+
+ if (mEncryptInterface.isModeSymmetric()) {
+ // symmetric encryption
+
+ boolean gotPassphrase = (mEncryptInterface.getPassphrase() != null
+ && mEncryptInterface.getPassphrase().length() != 0);
+ if (!gotPassphrase) {
+ AppMsg.makeText(getActivity(), R.string.passphrase_must_not_be_empty, AppMsg.STYLE_ALERT)
+ .show();
+ return;
+ }
+
+ if (!mEncryptInterface.getPassphrase().equals(mEncryptInterface.getPassphraseAgain())) {
+ AppMsg.makeText(getActivity(), R.string.passphrases_do_not_match, AppMsg.STYLE_ALERT).show();
+ return;
+ }
+ } else {
+ // asymmetric encryption
+
+ boolean gotEncryptionKeys = (mEncryptInterface.getEncryptionKeys() != null
+ && mEncryptInterface.getEncryptionKeys().length > 0);
+
+ if (!gotEncryptionKeys) {
+ AppMsg.makeText(getActivity(), R.string.select_encryption_key, AppMsg.STYLE_ALERT).show();
+ return;
+ }
+
+ if (!gotEncryptionKeys && mEncryptInterface.getSignatureKey() == 0) {
+ AppMsg.makeText(getActivity(), R.string.select_encryption_or_signature_key,
+ AppMsg.STYLE_ALERT).show();
+ return;
+ }
+
+ if (mEncryptInterface.getSignatureKey() != 0 &&
+ PassphraseCacheService.getCachedPassphrase(getActivity(),
+ mEncryptInterface.getSignatureKey()) == null) {
+ PassphraseDialogFragment.show(getActivity(), mEncryptInterface.getSignatureKey(),
+ new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) {
+ showOutputFileDialog();
+ }
+ }
+ });
+
+ return;
+ }
+ }
+
+ showOutputFileDialog();
+ }
+
+ private void encryptStart() {
+ // Send all information needed to service to edit key in other thread
+ Intent intent = new Intent(getActivity(), KeychainIntentService.class);
+
+ intent.setAction(KeychainIntentService.ACTION_ENCRYPT_SIGN);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_URI);
+
+ if (mEncryptInterface.isModeSymmetric()) {
+ Log.d(Constants.TAG, "Symmetric encryption enabled!");
+ String passphrase = mEncryptInterface.getPassphrase();
+ if (passphrase.length() == 0) {
+ passphrase = null;
+ }
+ data.putString(KeychainIntentService.ENCRYPT_SYMMETRIC_PASSPHRASE, passphrase);
+ } else {
+ data.putLong(KeychainIntentService.ENCRYPT_SIGNATURE_KEY_ID,
+ mEncryptInterface.getSignatureKey());
+ data.putLongArray(KeychainIntentService.ENCRYPT_ENCRYPTION_KEYS_IDS,
+ mEncryptInterface.getEncryptionKeys());
+ }
+
+ Log.d(Constants.TAG, "mInputFilename=" + mInputFilename + ", mOutputFilename="
+ + mOutputFilename);
+
+ data.putString(KeychainIntentService.ENCRYPT_INPUT_FILE, mInputFilename);
+ data.putString(KeychainIntentService.ENCRYPT_OUTPUT_FILE, mOutputFilename);
+
+ boolean useAsciiArmor = mAsciiArmor.isChecked();
+ data.putBoolean(KeychainIntentService.ENCRYPT_USE_ASCII_ARMOR, useAsciiArmor);
+
+ int compressionId = ((Choice) mFileCompression.getSelectedItem()).getId();
+ data.putInt(KeychainIntentService.ENCRYPT_COMPRESSION_ID, compressionId);
+// data.putBoolean(KeychainIntentService.ENCRYPT_GENERATE_SIGNATURE, mGenerateSignature);
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after encrypting is done in KeychainIntentService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(getActivity(),
+ getString(R.string.progress_encrypting), ProgressDialog.STYLE_HORIZONTAL) {
+ public void handleMessage(Message message) {
+ // handle messages by standard KeychainIntentServiceHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ AppMsg.makeText(getActivity(), R.string.encryption_successful,
+ AppMsg.STYLE_INFO).show();
+
+ if (mDeleteAfter.isChecked()) {
+ // Create and show dialog to delete original file
+ DeleteFileDialogFragment deleteFileDialog = DeleteFileDialogFragment
+ .newInstance(mInputFilename);
+ deleteFileDialog.show(getActivity().getSupportFragmentManager(), "deleteDialog");
+ }
+
+ if (mShareAfter.isChecked()) {
+ // Share encrypted file
+ Intent sendFileIntent = new Intent(Intent.ACTION_SEND);
+ sendFileIntent.setType("*/*");
+ sendFileIntent.putExtra(Intent.EXTRA_STREAM, Uri.parse(mOutputFilename));
+ startActivity(Intent.createChooser(sendFileIntent,
+ getString(R.string.title_send_file)));
+ }
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // show progress dialog
+ saveHandler.showProgressDialog(getActivity());
+
+ // start service with intent
+ getActivity().startService(intent);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case RESULT_CODE_FILE: {
+ if (resultCode == Activity.RESULT_OK && data != null) {
+ try {
+ String path = FileHelper.getPath(getActivity(), data.getData());
+ Log.d(Constants.TAG, "path=" + path);
+
+ mFilename.setText(path);
+ } catch (NullPointerException e) {
+ Log.e(Constants.TAG, "Nullpointer while retrieving path!");
+ }
+ }
+ return;
+ }
+
+ default: {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ break;
+ }
+ }
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptMessageFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptMessageFragment.java
new file mode 100644
index 000000000..ba11074fc
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptMessageFragment.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.app.Activity;
+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.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+import com.beardedhen.androidbootstrap.BootstrapButton;
+import com.devspark.appmsg.AppMsg;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.compatibility.ClipboardReflection;
+import org.sufficientlysecure.keychain.helper.Preferences;
+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;
+
+public class EncryptMessageFragment extends Fragment {
+ public static final String ARG_TEXT = "text";
+
+ private EditText mMessage = null;
+ private BootstrapButton mEncryptShare;
+ private BootstrapButton mEncryptClipboard;
+
+ private EncryptActivityInterface mEncryptInterface;
+
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ try {
+ mEncryptInterface = (EncryptActivityInterface) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString() + " must implement EncryptActivityInterface");
+ }
+ }
+
+ /**
+ * Inflate the layout for this fragment
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.encrypt_message_fragment, container, false);
+
+ mMessage = (EditText) view.findViewById(R.id.message);
+ mEncryptClipboard = (BootstrapButton) view.findViewById(R.id.action_encrypt_clipboard);
+ mEncryptShare = (BootstrapButton) view.findViewById(R.id.action_encrypt_share);
+ mEncryptClipboard.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ encryptClicked(true);
+ }
+ });
+ mEncryptShare.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ encryptClicked(false);
+ }
+ });
+
+ return view;
+ }
+
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ String text = getArguments().getString(ARG_TEXT);
+ if (text != null) {
+ mMessage.setText(text);
+ }
+ }
+
+ /**
+ * 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 encryptClicked(final boolean toClipboard) {
+ if (mEncryptInterface.isModeSymmetric()) {
+ // symmetric encryption
+
+ boolean gotPassphrase = (mEncryptInterface.getPassphrase() != null
+ && mEncryptInterface.getPassphrase().length() != 0);
+ if (!gotPassphrase) {
+ AppMsg.makeText(getActivity(), R.string.passphrase_must_not_be_empty, AppMsg.STYLE_ALERT)
+ .show();
+ return;
+ }
+
+ if (!mEncryptInterface.getPassphrase().equals(mEncryptInterface.getPassphraseAgain())) {
+ AppMsg.makeText(getActivity(), R.string.passphrases_do_not_match, AppMsg.STYLE_ALERT).show();
+ return;
+ }
+
+ } else {
+ // asymmetric encryption
+
+ boolean gotEncryptionKeys = (mEncryptInterface.getEncryptionKeys() != null
+ && mEncryptInterface.getEncryptionKeys().length > 0);
+
+ if (!gotEncryptionKeys && mEncryptInterface.getSignatureKey() == 0) {
+ AppMsg.makeText(getActivity(), R.string.select_encryption_or_signature_key,
+ AppMsg.STYLE_ALERT).show();
+ return;
+ }
+
+ if (mEncryptInterface.getSignatureKey() != 0 &&
+ PassphraseCacheService.getCachedPassphrase(getActivity(),
+ mEncryptInterface.getSignatureKey()) == null) {
+ PassphraseDialogFragment.show(getActivity(), mEncryptInterface.getSignatureKey(),
+ new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) {
+ encryptStart(toClipboard);
+ }
+ }
+ });
+
+ return;
+ }
+ }
+
+ encryptStart(toClipboard);
+ }
+
+ private void encryptStart(final boolean toClipboard) {
+ // Send all information needed to service to edit key in other thread
+ Intent intent = new Intent(getActivity(), KeychainIntentService.class);
+
+ intent.setAction(KeychainIntentService.ACTION_ENCRYPT_SIGN);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_BYTES);
+
+ String message = mMessage.getText().toString();
+
+ if (mEncryptInterface.isModeSymmetric()) {
+ Log.d(Constants.TAG, "Symmetric encryption enabled!");
+ String passphrase = mEncryptInterface.getPassphrase();
+ if (passphrase.length() == 0) {
+ passphrase = null;
+ }
+ data.putString(KeychainIntentService.ENCRYPT_SYMMETRIC_PASSPHRASE, passphrase);
+ } else {
+ data.putLong(KeychainIntentService.ENCRYPT_SIGNATURE_KEY_ID,
+ mEncryptInterface.getSignatureKey());
+ data.putLongArray(KeychainIntentService.ENCRYPT_ENCRYPTION_KEYS_IDS,
+ mEncryptInterface.getEncryptionKeys());
+
+ boolean signOnly = (mEncryptInterface.getEncryptionKeys() == null
+ || mEncryptInterface.getEncryptionKeys().length == 0);
+ if (signOnly) {
+ message = fixBadCharactersForGmail(message);
+ }
+ }
+
+ data.putByteArray(KeychainIntentService.ENCRYPT_MESSAGE_BYTES, message.getBytes());
+
+ data.putBoolean(KeychainIntentService.ENCRYPT_USE_ASCII_ARMOR, true);
+
+ int compressionId = Preferences.getPreferences(getActivity()).getDefaultMessageCompression();
+ data.putInt(KeychainIntentService.ENCRYPT_COMPRESSION_ID, compressionId);
+// data.putBoolean(KeychainIntentService.ENCRYPT_GENERATE_SIGNATURE, mGenerateSignature);
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after encrypting is done in KeychainIntentService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(getActivity(),
+ getString(R.string.progress_encrypting), ProgressDialog.STYLE_HORIZONTAL) {
+ public void handleMessage(Message message) {
+ // handle messages by standard KeychainIntentServiceHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ // get returned data bundle
+ Bundle data = message.getData();
+
+ String output = new String(data.getByteArray(KeychainIntentService.RESULT_BYTES));
+ Log.d(Constants.TAG, "output: " + output);
+
+ if (toClipboard) {
+ ClipboardReflection.copyToClipboard(getActivity(), output);
+ AppMsg.makeText(getActivity(),
+ R.string.encryption_to_clipboard_successful, AppMsg.STYLE_INFO)
+ .show();
+ } else {
+ Intent sendIntent = new Intent(Intent.ACTION_SEND);
+
+ // Type is set to text/plain so that encrypted messages can
+ // be sent with Whatsapp, Hangouts, SMS etc...
+ sendIntent.setType("text/plain");
+
+ sendIntent.putExtra(Intent.EXTRA_TEXT, output);
+ startActivity(Intent.createChooser(sendIntent,
+ getString(R.string.title_send_email)));
+ }
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // show progress dialog
+ saveHandler.showProgressDialog(getActivity());
+
+ // start service with intent
+ getActivity().startService(intent);
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptSymmetricFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptSymmetricFragment.java
new file mode 100644
index 000000000..8efa07953
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptSymmetricFragment.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+import org.sufficientlysecure.keychain.R;
+
+public class EncryptSymmetricFragment extends Fragment {
+
+ OnSymmetricKeySelection mPassphraseUpdateListener;
+
+ private EditText mPassphrase;
+ private EditText mPassphraseAgain;
+
+ // Container Activity must implement this interface
+ public interface OnSymmetricKeySelection {
+ public void onPassphraseUpdate(String passphrase);
+
+ public void onPassphraseAgainUpdate(String passphrase);
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ try {
+ mPassphraseUpdateListener = (OnSymmetricKeySelection) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString() + " must implement OnSymmetricKeySelection");
+ }
+ }
+
+ /**
+ * Inflate the layout for this fragment
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.encrypt_symmetric_fragment, container, false);
+
+ mPassphrase = (EditText) view.findViewById(R.id.passphrase);
+ mPassphraseAgain = (EditText) view.findViewById(R.id.passphraseAgain);
+ mPassphrase.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ // update passphrase in EncryptActivity
+ mPassphraseUpdateListener.onPassphraseUpdate(s.toString());
+ }
+ });
+ mPassphraseAgain.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ // update passphrase in EncryptActivity
+ mPassphraseUpdateListener.onPassphraseAgainUpdate(s.toString());
+ }
+ });
+
+ return view;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpAboutFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpAboutFragment.java
new file mode 100644
index 000000000..a484b57de
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpAboutFragment.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import org.sufficientlysecure.htmltextview.HtmlTextView;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.util.Log;
+
+
+public class HelpAboutFragment extends Fragment {
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.help_about_fragment, container, false);
+
+ TextView versionText = (TextView) view.findViewById(R.id.help_about_version);
+ versionText.setText(getString(R.string.help_about_version) + " " + getVersion());
+
+ HtmlTextView aboutTextView = (HtmlTextView) view.findViewById(R.id.help_about_text);
+
+ // load html from raw resource (Parsing handled by HtmlTextView library)
+ aboutTextView.setHtmlFromRawResource(getActivity(), R.raw.help_about);
+
+ // 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;
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpActivity.java
new file mode 100644
index 000000000..32f37a0a5
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpActivity.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.view.ViewPager;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.ActionBarActivity;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.ui.adapter.TabsAdapter;
+
+public class HelpActivity extends ActionBarActivity {
+ public static final String EXTRA_SELECTED_TAB = "selected_tab";
+
+ ViewPager mViewPager;
+ TabsAdapter mTabsAdapter;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.help_activity);
+
+ mViewPager = (ViewPager) findViewById(R.id.pager);
+
+ final ActionBar actionBar = getSupportActionBar();
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(false);
+ actionBar.setHomeButtonEnabled(false);
+ actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+
+ 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(HelpHtmlFragment.ARG_HTML_FILE, R.raw.help_start);
+ mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.help_tab_start)),
+ HelpHtmlFragment.class, startBundle, (selectedTab == 0));
+
+ Bundle faqBundle = new Bundle();
+ faqBundle.putInt(HelpHtmlFragment.ARG_HTML_FILE, R.raw.help_faq);
+ mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.help_tab_faq)),
+ HelpHtmlFragment.class, faqBundle, (selectedTab == 1));
+
+ Bundle nfcBundle = new Bundle();
+ nfcBundle.putInt(HelpHtmlFragment.ARG_HTML_FILE, R.raw.help_nfc_beam);
+ mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.help_tab_nfc_beam)),
+ HelpHtmlFragment.class, nfcBundle, (selectedTab == 2));
+
+ Bundle changelogBundle = new Bundle();
+ changelogBundle.putInt(HelpHtmlFragment.ARG_HTML_FILE, R.raw.help_changelog);
+ mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.help_tab_changelog)),
+ HelpHtmlFragment.class, changelogBundle, (selectedTab == 3));
+
+ mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.help_tab_about)),
+ HelpAboutFragment.class, null, (selectedTab == 4));
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpHtmlFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpHtmlFragment.java
new file mode 100644
index 000000000..6b3c51b08
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpHtmlFragment.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ScrollView;
+import org.sufficientlysecure.htmltextview.HtmlTextView;
+
+public class HelpHtmlFragment extends Fragment {
+ private Activity mActivity;
+
+ private int mHtmlFile;
+
+ public static final String ARG_HTML_FILE = "htmlFile";
+
+ /**
+ * Create a new instance of HelpHtmlFragment, providing "htmlFile" as an argument.
+ */
+ static HelpHtmlFragment newInstance(int htmlFile) {
+ HelpHtmlFragment f = new HelpHtmlFragment();
+
+ // Supply html raw file input as an argument.
+ Bundle args = new Bundle();
+ args.putInt(ARG_HTML_FILE, htmlFile);
+ f.setArguments(args);
+
+ return f;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ mActivity = getActivity();
+
+ mHtmlFile = getArguments().getInt(ARG_HTML_FILE);
+
+ ScrollView scroller = new ScrollView(mActivity);
+ HtmlTextView text = new HtmlTextView(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 from raw resource (Parsing handled by HtmlTextView library)
+ text.setHtmlFromRawResource(getActivity(), mHtmlFile);
+
+ // no flickering when clicking textview for Android < 4
+ text.setTextColor(getResources().getColor(android.R.color.black));
+
+ return scroller;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java
new file mode 100644
index 000000000..6ea79473a
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java
@@ -0,0 +1,489 @@
+/*
+ * Copyright (C) 2012-2014 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 android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.nfc.NdefMessage;
+import android.nfc.NfcAdapter;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.Parcelable;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.ActionBarActivity;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ArrayAdapter;
+
+import com.beardedhen.androidbootstrap.BootstrapButton;
+import com.devspark.appmsg.AppMsg;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.helper.ActionBarHelper;
+import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListEntry;
+import org.sufficientlysecure.keychain.ui.dialog.BadImportKeyDialogFragment;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+public class ImportKeysActivity extends ActionBarActivity implements ActionBar.OnNavigationListener {
+ public static final String ACTION_IMPORT_KEY = Constants.INTENT_PREFIX + "IMPORT_KEY";
+ public static final String ACTION_IMPORT_KEY_FROM_QR_CODE = Constants.INTENT_PREFIX
+ + "IMPORT_KEY_FROM_QR_CODE";
+ public static final String ACTION_IMPORT_KEY_FROM_KEYSERVER = Constants.INTENT_PREFIX
+ + "IMPORT_KEY_FROM_KEYSERVER";
+ public static final String ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN = Constants.INTENT_PREFIX
+ + "IMPORT_KEY_FROM_KEY_SERVER_AND_RETURN";
+
+ // Actions for internal use only:
+ public static final String ACTION_IMPORT_KEY_FROM_FILE = Constants.INTENT_PREFIX
+ + "IMPORT_KEY_FROM_FILE";
+ public static final String ACTION_IMPORT_KEY_FROM_NFC = Constants.INTENT_PREFIX
+ + "IMPORT_KEY_FROM_NFC";
+
+ // only used by ACTION_IMPORT_KEY
+ public static final String EXTRA_KEY_BYTES = "key_bytes";
+
+ // only used by ACTION_IMPORT_KEY_FROM_KEYSERVER
+ public static final String EXTRA_QUERY = "query";
+ public static final String EXTRA_KEY_ID = "key_id";
+ public static final String EXTRA_FINGERPRINT = "fingerprint";
+
+ // only used by ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN when used from OpenPgpService
+ public static final String EXTRA_PENDING_INTENT_DATA = "data";
+ private Intent mPendingIntentData;
+
+ // view
+ private ImportKeysListFragment mListFragment;
+ private String[] mNavigationStrings;
+ private Fragment mCurrentFragment;
+ private BootstrapButton mImportButton;
+
+ private static final Class[] NAVIGATION_CLASSES = new Class[]{
+ ImportKeysServerFragment.class,
+ ImportKeysFileFragment.class,
+ ImportKeysQrCodeFragment.class,
+ ImportKeysClipboardFragment.class,
+ ImportKeysNFCFragment.class
+ };
+
+ private int mCurrentNavPosition = -1;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.import_keys_activity);
+
+ mImportButton = (BootstrapButton) findViewById(R.id.import_import);
+ mImportButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ importKeys();
+ }
+ });
+
+ mNavigationStrings = getResources().getStringArray(R.array.import_action_list);
+
+ if (ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN.equals(getIntent().getAction())) {
+ setTitle(R.string.nav_import);
+ } else {
+ ActionBarHelper.setBackButton(this);
+ getSupportActionBar().setDisplayShowTitleEnabled(false);
+
+ // set drop down navigation
+ Context context = getSupportActionBar().getThemedContext();
+ ArrayAdapter<CharSequence> navigationAdapter = ArrayAdapter.createFromResource(context,
+ R.array.import_action_list, android.R.layout.simple_spinner_dropdown_item);
+ getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
+ getSupportActionBar().setListNavigationCallbacks(navigationAdapter, this);
+ }
+
+ handleActions(savedInstanceState, getIntent());
+ }
+
+ protected void handleActions(Bundle savedInstanceState, Intent intent) {
+ String action = intent.getAction();
+ Bundle extras = intent.getExtras();
+ Uri dataUri = intent.getData();
+ String scheme = intent.getScheme();
+
+ if (extras == null) {
+ extras = new Bundle();
+ }
+
+ if (Intent.ACTION_VIEW.equals(action)) {
+ // Android's Action when opening file associated to Keychain (see AndroidManifest.xml)
+ // override action to delegate it to Keychain's ACTION_IMPORT_KEY
+ action = ACTION_IMPORT_KEY;
+ }
+
+ if (scheme != null && scheme.toLowerCase(Locale.ENGLISH).equals(Constants.FINGERPRINT_SCHEME)) {
+ /* Scanning a fingerprint directly with Barcode Scanner */
+ loadFromFingerprintUri(savedInstanceState, dataUri);
+ } else if (ACTION_IMPORT_KEY.equals(action)) {
+ /* Keychain's own Actions */
+
+ // display file fragment
+ loadNavFragment(1, null);
+
+ if (dataUri != null) {
+ // action: directly load data
+ startListFragment(savedInstanceState, null, dataUri, null);
+ } else if (extras.containsKey(EXTRA_KEY_BYTES)) {
+ byte[] importData = intent.getByteArrayExtra(EXTRA_KEY_BYTES);
+
+ // action: directly load data
+ startListFragment(savedInstanceState, importData, null, null);
+ }
+ } else if (ACTION_IMPORT_KEY_FROM_KEYSERVER.equals(action)
+ || ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN.equals(action)) {
+
+ // only used for OpenPgpService
+ if (extras.containsKey(EXTRA_PENDING_INTENT_DATA)) {
+ mPendingIntentData = extras.getParcelable(EXTRA_PENDING_INTENT_DATA);
+ }
+ if (extras.containsKey(EXTRA_QUERY) || extras.containsKey(EXTRA_KEY_ID)) {
+ /* simple search based on query or key id */
+
+ String query = null;
+ if (extras.containsKey(EXTRA_QUERY)) {
+ query = extras.getString(EXTRA_QUERY);
+ } else if (extras.containsKey(EXTRA_KEY_ID)) {
+ long keyId = intent.getLongExtra(EXTRA_KEY_ID, 0);
+ if (keyId != 0) {
+ query = PgpKeyHelper.convertKeyIdToHex(keyId);
+ }
+ }
+
+ if (query != null && query.length() > 0) {
+ // display keyserver fragment with query
+ Bundle args = new Bundle();
+ args.putString(ImportKeysServerFragment.ARG_QUERY, query);
+ loadNavFragment(0, args);
+
+ // action: search immediately
+ startListFragment(savedInstanceState, null, null, query);
+ } else {
+ Log.e(Constants.TAG, "Query is empty!");
+ return;
+ }
+ } else if (extras.containsKey(EXTRA_FINGERPRINT)) {
+ /*
+ * search based on fingerprint, here we can enforce a check in the end
+ * if the right key has been downloaded
+ */
+
+ String fingerprint = intent.getStringExtra(EXTRA_FINGERPRINT);
+ loadFromFingerprint(savedInstanceState, fingerprint);
+ } else {
+ Log.e(Constants.TAG,
+ "IMPORT_KEY_FROM_KEYSERVER action needs to contain the 'query', 'key_id', or " +
+ "'fingerprint' extra!");
+ return;
+ }
+ } else if (ACTION_IMPORT_KEY_FROM_FILE.equals(action)) {
+
+ // NOTE: this only displays the appropriate fragment, no actions are taken
+ loadNavFragment(1, null);
+
+ // no immediate actions!
+ startListFragment(savedInstanceState, null, null, null);
+ } else if (ACTION_IMPORT_KEY_FROM_QR_CODE.equals(action)) {
+ // also exposed in AndroidManifest
+
+ // NOTE: this only displays the appropriate fragment, no actions are taken
+ loadNavFragment(2, null);
+
+ // no immediate actions!
+ startListFragment(savedInstanceState, null, null, null);
+ } else if (ACTION_IMPORT_KEY_FROM_NFC.equals(action)) {
+
+ // NOTE: this only displays the appropriate fragment, no actions are taken
+ loadNavFragment(3, null);
+
+ // no immediate actions!
+ startListFragment(savedInstanceState, null, null, null);
+ } else {
+ startListFragment(savedInstanceState, null, null, null);
+ }
+ }
+
+ private void startListFragment(Bundle savedInstanceState, byte[] bytes, Uri dataUri, String serverQuery) {
+ // However, if we're being restored from a previous state,
+ // then we don't need to do anything and should return or else
+ // we could end up with overlapping fragments.
+ if (savedInstanceState != null) {
+ return;
+ }
+
+ // Create an instance of the fragment
+ mListFragment = ImportKeysListFragment.newInstance(bytes, dataUri, serverQuery);
+
+ // Add the fragment to the 'fragment_container' FrameLayout
+ // NOTE: We use commitAllowingStateLoss() to prevent weird crashes!
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.import_keys_list_container, mListFragment)
+ .commitAllowingStateLoss();
+ // do it immediately!
+ getSupportFragmentManager().executePendingTransactions();
+ }
+
+ /**
+ * "Basically, when using a list navigation, onNavigationItemSelected() is automatically
+ * called when your activity is created/re-created, whether you like it or not. To prevent
+ * your Fragment's onCreateView() from being called twice, this initial automatic call to
+ * onNavigationItemSelected() should check whether the Fragment is already in existence
+ * inside your Activity."
+ * <p/>
+ * from http://stackoverflow.com/a/14295474
+ * <p/>
+ * In our case, if we start ImportKeysActivity with parameters to directly search using a fingerprint,
+ * the fragment would be loaded twice resulting in the query being empty after the second load.
+ * <p/>
+ * Our solution:
+ * To prevent that a fragment will be loaded again even if it was already loaded loadNavFragment
+ * checks against mCurrentNavPosition.
+ *
+ * @param itemPosition
+ * @param itemId
+ * @return
+ */
+ @Override
+ public boolean onNavigationItemSelected(int itemPosition, long itemId) {
+ Log.d(Constants.TAG, "onNavigationItemSelected");
+
+ loadNavFragment(itemPosition, null);
+
+ return true;
+ }
+
+ private void loadNavFragment(int itemPosition, Bundle args) {
+ if (mCurrentNavPosition != itemPosition) {
+ if (ActionBar.NAVIGATION_MODE_LIST == getSupportActionBar().getNavigationMode()) {
+ getSupportActionBar().setSelectedNavigationItem(itemPosition);
+ }
+ loadFragment(NAVIGATION_CLASSES[itemPosition], args, mNavigationStrings[itemPosition]);
+ mCurrentNavPosition = itemPosition;
+ }
+ }
+
+ private void loadFragment(Class<?> clss, Bundle args, String tag) {
+ mCurrentFragment = Fragment.instantiate(this, clss.getName(), args);
+
+ FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
+ // Replace whatever is in the fragment container with this fragment
+ // and give the fragment a tag name equal to the string at the position selected
+ ft.replace(R.id.import_navigation_fragment, mCurrentFragment, tag);
+ // Apply changes
+ ft.commit();
+ }
+
+ public void loadFromFingerprintUri(Bundle savedInstanceState, Uri dataUri) {
+ String fingerprint = dataUri.toString().split(":")[1].toLowerCase(Locale.ENGLISH);
+
+ Log.d(Constants.TAG, "fingerprint: " + fingerprint);
+
+ loadFromFingerprint(savedInstanceState, fingerprint);
+ }
+
+ public void loadFromFingerprint(Bundle savedInstanceState, String fingerprint) {
+ if (fingerprint == null || fingerprint.length() < 40) {
+ AppMsg.makeText(this, R.string.import_qr_code_too_short_fingerprint,
+ AppMsg.STYLE_ALERT).show();
+ return;
+ }
+
+ String query = "0x" + fingerprint;
+
+ // display keyserver fragment with query
+ Bundle args = new Bundle();
+ args.putString(ImportKeysServerFragment.ARG_QUERY, query);
+ args.putBoolean(ImportKeysServerFragment.ARG_DISABLE_QUERY_EDIT, true);
+ loadNavFragment(0, args);
+
+ // action: search directly
+ startListFragment(savedInstanceState, null, null, query);
+ }
+
+ public void loadCallback(byte[] importData, Uri dataUri, String serverQuery, String keyServer) {
+ mListFragment.loadNew(importData, dataUri, serverQuery, keyServer);
+ }
+
+ /**
+ * Import keys with mImportData
+ */
+ public void importKeys() {
+ // Message is received after importing is done in KeychainIntentService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(
+ this,
+ getString(R.string.progress_importing),
+ ProgressDialog.STYLE_HORIZONTAL) {
+ public void handleMessage(Message message) {
+ // handle messages by standard KeychainIntentServiceHandler 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) {
+ String addedStr = getResources().getQuantityString(
+ R.plurals.keys_added_and_updated_1, added, added);
+ String updatedStr = getResources().getQuantityString(
+ R.plurals.keys_added_and_updated_2, updated, updated);
+ toastMessage = addedStr + updatedStr;
+ } else if (added > 0) {
+ toastMessage = getResources().getQuantityString(R.plurals.keys_added,
+ added, added);
+ } else if (updated > 0) {
+ toastMessage = getResources().getQuantityString(R.plurals.keys_updated,
+ updated, updated);
+ } else {
+ toastMessage = getString(R.string.no_keys_added_or_updated);
+ }
+ AppMsg.makeText(ImportKeysActivity.this, toastMessage, AppMsg.STYLE_INFO)
+ .show();
+ if (bad > 0) {
+ BadImportKeyDialogFragment badImportKeyDialogFragment =
+ BadImportKeyDialogFragment.newInstance(bad);
+ badImportKeyDialogFragment.show(getSupportFragmentManager(), "badKeyDialog");
+ }
+
+ if (ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN.equals(getIntent().getAction())) {
+ ImportKeysActivity.this.setResult(Activity.RESULT_OK, mPendingIntentData);
+ finish();
+ }
+ }
+ }
+ };
+
+ if (mListFragment.getKeyBytes() != null || mListFragment.getDataUri() != 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.setAction(KeychainIntentService.ACTION_IMPORT_KEYRING);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ // get selected key entries
+ ArrayList<ImportKeysListEntry> selectedEntries = mListFragment.getSelectedData();
+ data.putParcelableArrayList(KeychainIntentService.IMPORT_KEY_LIST, selectedEntries);
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // 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 if (mListFragment.getServerQuery() != null) {
+ // Send all information needed to service to query keys in other thread
+ Intent intent = new Intent(this, KeychainIntentService.class);
+
+ intent.setAction(KeychainIntentService.ACTION_DOWNLOAD_AND_IMPORT_KEYS);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ data.putString(KeychainIntentService.DOWNLOAD_KEY_SERVER, mListFragment.getKeyServer());
+
+ // get selected key entries
+ ArrayList<ImportKeysListEntry> selectedEntries = mListFragment.getSelectedData();
+ data.putParcelableArrayList(KeychainIntentService.DOWNLOAD_KEY_LIST, selectedEntries);
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // 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 {
+ AppMsg.makeText(this, R.string.error_nothing_import, AppMsg.STYLE_ALERT).show();
+ }
+ }
+
+ /**
+ * NFC
+ */
+ @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());
+ }
+ }
+
+ /**
+ * NFC
+ */
+ @Override
+ public void onNewIntent(Intent intent) {
+ // onResume gets called after this to handle the intent
+ setIntent(intent);
+ }
+
+ /**
+ * NFC: Parses the NDEF Message from the intent and prints to the TextView
+ */
+ @SuppressLint("NewApi")
+ 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_KEY);
+ importIntent.putExtra(ImportKeysActivity.EXTRA_KEY_BYTES, receivedKeyringBytes);
+
+ handleActions(null, importIntent);
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysClipboardFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysClipboardFragment.java
new file mode 100644
index 000000000..28e2091a9
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysClipboardFragment.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import com.beardedhen.androidbootstrap.BootstrapButton;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.compatibility.ClipboardReflection;
+
+import java.util.Locale;
+
+public class ImportKeysClipboardFragment extends Fragment {
+
+ private ImportKeysActivity mImportActivity;
+ private BootstrapButton mButton;
+
+ /**
+ * Creates new instance of this fragment
+ */
+ public static ImportKeysClipboardFragment newInstance() {
+ ImportKeysClipboardFragment frag = new ImportKeysClipboardFragment();
+
+ Bundle args = new Bundle();
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ /**
+ * Inflate the layout for this fragment
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.import_keys_clipboard_fragment, container, false);
+
+ mButton = (BootstrapButton) view.findViewById(R.id.import_clipboard_button);
+ mButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ CharSequence clipboardText = ClipboardReflection.getClipboardText(getActivity());
+ String sendText = "";
+ if (clipboardText != null) {
+ sendText = clipboardText.toString();
+ if (sendText.toLowerCase(Locale.ENGLISH).startsWith(Constants.FINGERPRINT_SCHEME)) {
+ mImportActivity.loadFromFingerprintUri(null, Uri.parse(sendText));
+ return;
+ }
+ }
+ mImportActivity.loadCallback(sendText.getBytes(), null, null, null);
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mImportActivity = (ImportKeysActivity) getActivity();
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java
new file mode 100644
index 000000000..31d5f3fd0
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import com.beardedhen.androidbootstrap.BootstrapButton;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.helper.FileHelper;
+
+public class ImportKeysFileFragment extends Fragment {
+ private ImportKeysActivity mImportActivity;
+ private BootstrapButton mBrowse;
+
+ /**
+ * Creates new instance of this fragment
+ */
+ public static ImportKeysFileFragment newInstance() {
+ ImportKeysFileFragment frag = new ImportKeysFileFragment();
+
+ Bundle args = new Bundle();
+
+ frag.setArguments(args);
+ return frag;
+ }
+
+ /**
+ * Inflate the layout for this fragment
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.import_keys_file_fragment, container, false);
+
+ mBrowse = (BootstrapButton) view.findViewById(R.id.import_keys_file_browse);
+
+ mBrowse.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ // open .asc or .gpg files
+ // setting it to text/plain prevents Cynaogenmod's file manager from selecting asc
+ // or gpg types!
+ FileHelper.openFile(ImportKeysFileFragment.this, Constants.Path.APP_DIR + "/",
+ "*/*", Id.request.filename);
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mImportActivity = (ImportKeysActivity) getActivity();
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode & 0xFFFF) {
+ case Id.request.filename: {
+ if (resultCode == Activity.RESULT_OK && data != null) {
+
+ // load data
+ mImportActivity.loadCallback(null, data.getData(), null, null);
+ }
+
+ break;
+ }
+
+ default:
+ super.onActivityResult(requestCode, resultCode, data);
+
+ break;
+ }
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java
new file mode 100644
index 000000000..077fa0cab
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.app.Activity;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.ListFragment;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.Loader;
+import android.view.View;
+import android.widget.ListView;
+import com.devspark.appmsg.AppMsg;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.helper.Preferences;
+import org.sufficientlysecure.keychain.ui.adapter.AsyncTaskResultWrapper;
+import org.sufficientlysecure.keychain.ui.adapter.ImportKeysAdapter;
+import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListEntry;
+import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListLoader;
+import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListServerLoader;
+import org.sufficientlysecure.keychain.util.InputData;
+import org.sufficientlysecure.keychain.util.KeyServer;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ImportKeysListFragment extends ListFragment implements
+ LoaderManager.LoaderCallbacks<AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>> {
+ private static final String ARG_DATA_URI = "uri";
+ private static final String ARG_BYTES = "bytes";
+ private static final String ARG_SERVER_QUERY = "query";
+
+ private Activity mActivity;
+ private ImportKeysAdapter mAdapter;
+
+ private byte[] mKeyBytes;
+ private Uri mDataUri;
+ private String mServerQuery;
+ private String mKeyServer;
+
+ private static final int LOADER_ID_BYTES = 0;
+ private static final int LOADER_ID_SERVER_QUERY = 1;
+
+ public byte[] getKeyBytes() {
+ return mKeyBytes;
+ }
+
+ public Uri getDataUri() {
+ return mDataUri;
+ }
+
+ public String getServerQuery() {
+ return mServerQuery;
+ }
+
+ public String getKeyServer() {
+ return mKeyServer;
+ }
+
+ public List<ImportKeysListEntry> getData() {
+ return mAdapter.getData();
+ }
+
+ public ArrayList<ImportKeysListEntry> getSelectedData() {
+ return mAdapter.getSelectedData();
+ }
+
+ /**
+ * Creates new instance of this fragment
+ */
+ public static ImportKeysListFragment newInstance(byte[] bytes, Uri dataUri, String serverQuery) {
+ ImportKeysListFragment frag = new ImportKeysListFragment();
+
+ Bundle args = new Bundle();
+ args.putByteArray(ARG_BYTES, bytes);
+ args.putParcelable(ARG_DATA_URI, dataUri);
+ args.putString(ARG_SERVER_QUERY, serverQuery);
+
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ /**
+ * Define Adapter and Loader on create of Activity
+ */
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mActivity = getActivity();
+
+ // 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_nothing_import));
+
+ // Create an empty adapter we will use to display the loaded data.
+ mAdapter = new ImportKeysAdapter(mActivity);
+ setListAdapter(mAdapter);
+
+ mDataUri = getArguments().getParcelable(ARG_DATA_URI);
+ mKeyBytes = getArguments().getByteArray(ARG_BYTES);
+ mServerQuery = getArguments().getString(ARG_SERVER_QUERY);
+
+ // TODO: this is used when scanning QR Code. Currently it simply uses keyserver nr 0
+ mKeyServer = Preferences.getPreferences(getActivity())
+ .getKeyServers()[0];
+
+ if (mDataUri != null || mKeyBytes != null) {
+ // Start out with a progress indicator.
+ setListShown(false);
+
+ // Prepare the loader. Either re-connect with an existing one,
+ // or start a new one.
+ // give arguments to onCreateLoader()
+ getLoaderManager().initLoader(LOADER_ID_BYTES, null, this);
+ }
+
+ if (mServerQuery != null && mKeyServer != null) {
+ // Start out with a progress indicator.
+ setListShown(false);
+
+ // Prepare the loader. Either re-connect with an existing one,
+ // or start a new one.
+ // give arguments to onCreateLoader()
+ getLoaderManager().initLoader(LOADER_ID_SERVER_QUERY, null, this);
+ }
+ }
+
+ @Override
+ public void onListItemClick(ListView l, View v, int position, long id) {
+ super.onListItemClick(l, v, position, id);
+
+ // Select checkbox!
+ // Update underlying data and notify adapter of change. The adapter will
+ // update the view automatically.
+ ImportKeysListEntry entry = mAdapter.getItem(position);
+ entry.setSelected(!entry.isSelected());
+ mAdapter.notifyDataSetChanged();
+ }
+
+ public void loadNew(byte[] keyBytes, Uri dataUri, String serverQuery, String keyServer) {
+ mKeyBytes = keyBytes;
+ mDataUri = dataUri;
+ mServerQuery = serverQuery;
+ mKeyServer = keyServer;
+
+ if (mKeyBytes != null || mDataUri != null) {
+ // Start out with a progress indicator.
+ setListShown(false);
+
+ getLoaderManager().restartLoader(LOADER_ID_BYTES, null, this);
+ }
+
+ if (mServerQuery != null && mKeyServer != null) {
+ // Start out with a progress indicator.
+ setListShown(false);
+
+ getLoaderManager().restartLoader(LOADER_ID_SERVER_QUERY, null, this);
+ }
+ }
+
+ @Override
+ public Loader<AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>>
+ onCreateLoader(int id, Bundle args) {
+ switch (id) {
+ case LOADER_ID_BYTES: {
+ InputData inputData = getInputData(mKeyBytes, mDataUri);
+ return new ImportKeysListLoader(mActivity, inputData);
+ }
+ case LOADER_ID_SERVER_QUERY: {
+ return new ImportKeysListServerLoader(getActivity(), mServerQuery, mKeyServer);
+ }
+
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public void onLoadFinished(Loader<AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>> loader,
+ AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>> data) {
+ // Swap the new cursor in. (The framework will take care of closing the
+ // old cursor once we return.)
+
+ Log.d(Constants.TAG, "data: " + data.getResult());
+
+ // swap in the real data!
+ mAdapter.setData(data.getResult());
+ mAdapter.notifyDataSetChanged();
+
+ setListAdapter(mAdapter);
+
+ // The list should now be shown.
+ if (isResumed()) {
+ setListShown(true);
+ } else {
+ setListShownNoAnimation(true);
+ }
+
+ Exception error = data.getError();
+
+ switch (loader.getId()) {
+ case LOADER_ID_BYTES:
+
+ if (error == null) {
+ // No error
+ } else if (error instanceof ImportKeysListLoader.FileHasNoContent) {
+ AppMsg.makeText(getActivity(), R.string.error_import_file_no_content,
+ AppMsg.STYLE_ALERT).show();
+ } else if (error instanceof ImportKeysListLoader.NonPgpPart) {
+ AppMsg.makeText(getActivity(),
+ ((ImportKeysListLoader.NonPgpPart) error).getCount() + " " + getResources().
+ getQuantityString(R.plurals.error_import_non_pgp_part,
+ ((ImportKeysListLoader.NonPgpPart) error).getCount()),
+ new AppMsg.Style(AppMsg.LENGTH_LONG, R.color.confirm)).show();
+ } else {
+ AppMsg.makeText(getActivity(), R.string.error_generic_report_bug,
+ new AppMsg.Style(AppMsg.LENGTH_LONG, R.color.alert)).show();
+ }
+ break;
+
+ case LOADER_ID_SERVER_QUERY:
+
+ if (error == null) {
+ AppMsg.makeText(
+ getActivity(), getResources().getQuantityString(R.plurals.keys_found,
+ mAdapter.getCount(), mAdapter.getCount()),
+ AppMsg.STYLE_INFO
+ ).show();
+ } else if (error instanceof KeyServer.InsufficientQuery) {
+ AppMsg.makeText(getActivity(), R.string.error_keyserver_insufficient_query,
+ AppMsg.STYLE_ALERT).show();
+ } else if (error instanceof KeyServer.QueryException) {
+ AppMsg.makeText(getActivity(), R.string.error_keyserver_query,
+ AppMsg.STYLE_ALERT).show();
+ } else if (error instanceof KeyServer.TooManyResponses) {
+ AppMsg.makeText(getActivity(), R.string.error_keyserver_too_many_responses,
+ AppMsg.STYLE_ALERT).show();
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>> loader) {
+ switch (loader.getId()) {
+ case LOADER_ID_BYTES:
+ // Clear the data in the adapter.
+ mAdapter.clear();
+ break;
+ case LOADER_ID_SERVER_QUERY:
+ // Clear the data in the adapter.
+ mAdapter.clear();
+ break;
+ default:
+ break;
+ }
+ }
+
+ private InputData getInputData(byte[] importBytes, Uri dataUri) {
+ InputData inputData = null;
+ if (importBytes != null) {
+ inputData = new InputData(new ByteArrayInputStream(importBytes), importBytes.length);
+ } else if (dataUri != null) {
+ try {
+ InputStream inputStream = getActivity().getContentResolver().openInputStream(dataUri);
+ int length = inputStream.available();
+
+ inputData = new InputData(inputStream, length);
+ } catch (FileNotFoundException e) {
+ Log.e(Constants.TAG, "FileNotFoundException!", e);
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "IOException!", e);
+ }
+ }
+
+ return inputData;
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysNFCFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysNFCFragment.java
new file mode 100644
index 000000000..110647284
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysNFCFragment.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import com.beardedhen.androidbootstrap.BootstrapButton;
+import org.sufficientlysecure.keychain.R;
+
+public class ImportKeysNFCFragment extends Fragment {
+
+ private BootstrapButton mButton;
+
+ /**
+ * Creates new instance of this fragment
+ */
+ public static ImportKeysNFCFragment newInstance() {
+ ImportKeysNFCFragment frag = new ImportKeysNFCFragment();
+
+ Bundle args = new Bundle();
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ /**
+ * Inflate the layout for this fragment
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.import_keys_nfc_fragment, container, false);
+
+ mButton = (BootstrapButton) view.findViewById(R.id.import_nfc_button);
+ mButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ // show nfc help
+ Intent intent = new Intent(getActivity(), HelpActivity.class);
+ intent.putExtra(HelpActivity.EXTRA_SELECTED_TAB, 2);
+ startActivityForResult(intent, 0);
+ }
+ });
+
+ return view;
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysQrCodeFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysQrCodeFragment.java
new file mode 100644
index 000000000..8b553d273
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysQrCodeFragment.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import com.google.zxing.integration.android.IntentResult;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.beardedhen.androidbootstrap.BootstrapButton;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.util.IntentIntegratorSupportV4;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+public class ImportKeysQrCodeFragment extends Fragment {
+
+ private ImportKeysActivity mImportActivity;
+ private BootstrapButton mButton;
+ private TextView mText;
+ private ProgressBar mProgress;
+
+ private String[] mScannedContent;
+
+ /**
+ * Creates new instance of this fragment
+ */
+ public static ImportKeysQrCodeFragment newInstance() {
+ ImportKeysQrCodeFragment frag = new ImportKeysQrCodeFragment();
+
+ Bundle args = new Bundle();
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ /**
+ * Inflate the layout for this fragment
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.import_keys_qr_code_fragment, container, false);
+
+ mButton = (BootstrapButton) view.findViewById(R.id.import_qrcode_button);
+ mText = (TextView) view.findViewById(R.id.import_qrcode_text);
+ mProgress = (ProgressBar) view.findViewById(R.id.import_qrcode_progress);
+
+ mButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ // scan using xzing's Barcode Scanner
+ new IntentIntegratorSupportV4(ImportKeysQrCodeFragment.this).initiateScan();
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mImportActivity = (ImportKeysActivity) getActivity();
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode & 0xFFFF) {
+ case IntentIntegratorSupportV4.REQUEST_CODE: {
+ IntentResult scanResult = IntentIntegratorSupportV4.parseActivityResult(requestCode,
+ resultCode, data);
+ if (scanResult != null && scanResult.getFormatName() != null) {
+ String scannedContent = scanResult.getContents();
+
+ Log.d(Constants.TAG, "scannedContent: " + scannedContent);
+
+ // look if it's fingerprint only
+ if (scannedContent.toLowerCase(Locale.ENGLISH).startsWith(Constants.FINGERPRINT_SCHEME)) {
+ importFingerprint(Uri.parse(scanResult.getContents()));
+ return;
+ }
+
+ // look if it is the whole key
+ String[] parts = scannedContent.split(",");
+ if (parts.length == 3) {
+ importParts(parts);
+ return;
+ }
+
+ // is this a full key encoded as qr code?
+ if (scannedContent.startsWith("-----BEGIN PGP")) {
+ mImportActivity.loadCallback(scannedContent.getBytes(), null, null, null);
+ return;
+ }
+
+ // fail...
+ Toast.makeText(getActivity(), R.string.import_qr_code_wrong, Toast.LENGTH_LONG)
+ .show();
+ }
+
+ break;
+ }
+
+ default:
+ super.onActivityResult(requestCode, resultCode, data);
+
+ break;
+ }
+ }
+
+ public void importFingerprint(Uri dataUri) {
+ mImportActivity.loadFromFingerprintUri(null, dataUri);
+ }
+
+ private void importParts(String[] parts) {
+ int counter = Integer.valueOf(parts[0]);
+ int size = Integer.valueOf(parts[1]);
+ String content = parts[2];
+
+ Log.d(Constants.TAG, "" + counter);
+ Log.d(Constants.TAG, "" + size);
+ Log.d(Constants.TAG, "" + content);
+
+ // first qr code -> setup
+ if (counter == 0) {
+ mScannedContent = new String[size];
+ mProgress.setMax(size);
+ mProgress.setVisibility(View.VISIBLE);
+ mText.setVisibility(View.VISIBLE);
+ }
+
+ if (mScannedContent == null || counter > mScannedContent.length) {
+ Toast.makeText(getActivity(), R.string.import_qr_code_start_with_one, Toast.LENGTH_LONG)
+ .show();
+ return;
+ }
+
+ // save scanned content
+ mScannedContent[counter] = content;
+
+ // get missing numbers
+ ArrayList<Integer> missing = new ArrayList<Integer>();
+ for (int i = 0; i < mScannedContent.length; i++) {
+ if (mScannedContent[i] == null) {
+ missing.add(i);
+ }
+ }
+
+ // update progress and text
+ int alreadyScanned = mScannedContent.length - missing.size();
+ mProgress.setProgress(alreadyScanned);
+
+ String missingString = "";
+ for (int m : missing) {
+ if (!missingString.equals("")) {
+ missingString += ", ";
+ }
+ missingString += String.valueOf(m + 1);
+ }
+
+ String missingText = getResources().getQuantityString(R.plurals.import_qr_code_missing,
+ missing.size(), missingString);
+ mText.setText(missingText);
+
+ // finished!
+ if (missing.size() == 0) {
+ mText.setText(R.string.import_qr_code_finished);
+ String result = "";
+ for (String in : mScannedContent) {
+ result += in;
+ }
+ mImportActivity.loadCallback(result.getBytes(), null, null, null);
+ }
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysServerFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysServerFragment.java
new file mode 100644
index 000000000..3eb463dac
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysServerFragment.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.TextView;
+import com.beardedhen.androidbootstrap.BootstrapButton;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.helper.Preferences;
+import org.sufficientlysecure.keychain.util.Log;
+
+public class ImportKeysServerFragment extends Fragment {
+ public static final String ARG_QUERY = "query";
+ public static final String ARG_KEY_SERVER = "key_server";
+ public static final String ARG_DISABLE_QUERY_EDIT = "disable_query_edit";
+
+ private ImportKeysActivity mImportActivity;
+
+ private BootstrapButton mSearchButton;
+ private EditText mQueryEditText;
+ private Spinner mServerSpinner;
+ private ArrayAdapter<String> mServerAdapter;
+
+ /**
+ * Creates new instance of this fragment
+ */
+ public static ImportKeysServerFragment newInstance(String query, String keyServer) {
+ ImportKeysServerFragment frag = new ImportKeysServerFragment();
+
+ Bundle args = new Bundle();
+ args.putString(ARG_QUERY, query);
+ args.putString(ARG_KEY_SERVER, keyServer);
+
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ /**
+ * Inflate the layout for this fragment
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.import_keys_server_fragment, container, false);
+
+ mSearchButton = (BootstrapButton) view.findViewById(R.id.import_server_search);
+ mQueryEditText = (EditText) view.findViewById(R.id.import_server_query);
+ mServerSpinner = (Spinner) view.findViewById(R.id.import_server_spinner);
+
+ // add keyservers to spinner
+ mServerAdapter = new ArrayAdapter<String>(getActivity(),
+ android.R.layout.simple_spinner_item, Preferences.getPreferences(getActivity())
+ .getKeyServers());
+ mServerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mServerSpinner.setAdapter(mServerAdapter);
+ if (mServerAdapter.getCount() > 0) {
+ mServerSpinner.setSelection(0);
+ } else {
+ mSearchButton.setEnabled(false);
+ }
+
+ mSearchButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ String query = mQueryEditText.getText().toString();
+ String keyServer = (String) mServerSpinner.getSelectedItem();
+ search(query, keyServer);
+
+ // close keyboard after pressing search
+ InputMethodManager imm =
+ (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(mQueryEditText.getWindowToken(), 0);
+ }
+ });
+
+ mQueryEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_SEARCH) {
+ String query = mQueryEditText.getText().toString();
+ String keyServer = (String) mServerSpinner.getSelectedItem();
+ search(query, keyServer);
+
+ // Don't return true to let the keyboard close itself after pressing search
+ return false;
+ }
+ return false;
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mImportActivity = (ImportKeysActivity) getActivity();
+
+ // set displayed values
+ if (getArguments() != null) {
+ if (getArguments().containsKey(ARG_QUERY)) {
+ String query = getArguments().getString(ARG_QUERY);
+ mQueryEditText.setText(query, TextView.BufferType.EDITABLE);
+
+ Log.d(Constants.TAG, "query: " + query);
+ }
+
+ if (getArguments().containsKey(ARG_KEY_SERVER)) {
+ String keyServer = getArguments().getString(ARG_KEY_SERVER);
+ int keyServerPos = mServerAdapter.getPosition(keyServer);
+ mServerSpinner.setSelection(keyServerPos);
+
+ Log.d(Constants.TAG, "keyServer: " + keyServer);
+ }
+
+ if (getArguments().getBoolean(ARG_DISABLE_QUERY_EDIT, false)) {
+ mQueryEditText.setEnabled(false);
+ }
+ }
+ }
+
+ private void search(String query, String keyServer) {
+ mImportActivity.loadCallback(null, null, query, keyServer);
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListActivity.java
new file mode 100644
index 000000000..8db643583
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListActivity.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.helper.ExportHelper;
+
+public class KeyListActivity extends DrawerActivity {
+
+ ExportHelper mExportHelper;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mExportHelper = new ExportHelper(this);
+
+ setContentView(R.layout.key_list_activity);
+
+ // now setup navigation drawer in DrawerActivity...
+ setupDrawerNavigation(savedInstanceState);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ getMenuInflater().inflate(R.menu.key_list, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_key_list_import:
+ importKeys();
+ return true;
+
+ case R.id.menu_key_list_create:
+ createKey();
+ return true;
+
+ case R.id.menu_key_list_create_expert:
+ createKeyExpert();
+ return true;
+
+ case R.id.menu_key_list_export:
+ mExportHelper.showExportKeysDialog(null, Constants.Path.APP_DIR_FILE, true);
+ return true;
+
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ private void importKeys() {
+ Intent intent = new Intent(this, ImportKeysActivity.class);
+ startActivityForResult(intent, 0);
+ }
+
+ private void createKey() {
+ Intent intent = new Intent(this, EditKeyActivity.class);
+ intent.setAction(EditKeyActivity.ACTION_CREATE_KEY);
+ intent.putExtra(EditKeyActivity.EXTRA_GENERATE_DEFAULT_KEYS, true);
+ intent.putExtra(EditKeyActivity.EXTRA_USER_IDS, ""); // show user id view
+ startActivityForResult(intent, 0);
+ }
+
+ private void createKeyExpert() {
+ Intent intent = new Intent(this, EditKeyActivity.class);
+ intent.setAction(EditKeyActivity.ACTION_CREATE_KEY);
+ startActivityForResult(intent, 0);
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java
new file mode 100644
index 000000000..3e2c96464
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java
@@ -0,0 +1,700 @@
+/*
+ * Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Messenger;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.support.v4.view.MenuItemCompat;
+import android.support.v7.app.ActionBarActivity;
+import android.support.v7.widget.SearchView;
+import android.text.TextUtils;
+import android.view.ActionMode;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.animation.AnimationUtils;
+import android.widget.AbsListView.MultiChoiceModeListener;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+import com.beardedhen.androidbootstrap.BootstrapButton;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.helper.ExportHelper;
+import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData;
+import org.sufficientlysecure.keychain.ui.adapter.HighlightQueryCursorAdapter;
+import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment;
+import org.sufficientlysecure.keychain.util.Log;
+import se.emilsjolander.stickylistheaders.ApiLevelTooLowException;
+import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter;
+import se.emilsjolander.stickylistheaders.StickyListHeadersListView;
+
+import java.util.HashMap;
+
+/**
+ * Public key list with sticky list headers. It does _not_ extend ListFragment because it uses
+ * StickyListHeaders library which does not extend upon ListView.
+ */
+public class KeyListFragment extends Fragment
+ implements SearchView.OnQueryTextListener, AdapterView.OnItemClickListener,
+ LoaderManager.LoaderCallbacks<Cursor> {
+
+ private KeyListAdapter mAdapter;
+ private StickyListHeadersListView mStickyList;
+
+ // rebuild functionality of ListFragment, http://stackoverflow.com/a/12504097
+ boolean mListShown;
+ View mProgressContainer;
+ View mListContainer;
+
+ private String mCurQuery;
+ private SearchView mSearchView;
+ // empty list layout
+ private BootstrapButton mButtonEmptyCreate;
+ private BootstrapButton mButtonEmptyImport;
+
+
+ /**
+ * Load custom layout with StickyListView from library
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View root = inflater.inflate(R.layout.key_list_fragment, container, false);
+
+ mStickyList = (StickyListHeadersListView) root.findViewById(R.id.key_list_list);
+ mStickyList.setOnItemClickListener(this);
+
+
+ // empty view
+ mButtonEmptyCreate = (BootstrapButton) root.findViewById(R.id.key_list_empty_button_create);
+ mButtonEmptyCreate.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(getActivity(), EditKeyActivity.class);
+ intent.setAction(EditKeyActivity.ACTION_CREATE_KEY);
+ intent.putExtra(EditKeyActivity.EXTRA_GENERATE_DEFAULT_KEYS, true);
+ intent.putExtra(EditKeyActivity.EXTRA_USER_IDS, ""); // show user id view
+ startActivityForResult(intent, 0);
+ }
+ });
+ mButtonEmptyImport = (BootstrapButton) root.findViewById(R.id.key_list_empty_button_import);
+ mButtonEmptyImport.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(getActivity(), ImportKeysActivity.class);
+ intent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_FILE);
+ startActivityForResult(intent, 0);
+ }
+ });
+
+ // rebuild functionality of ListFragment, http://stackoverflow.com/a/12504097
+ mListContainer = root.findViewById(R.id.key_list_list_container);
+ mProgressContainer = root.findViewById(R.id.key_list_progress_container);
+ mListShown = true;
+
+ return root;
+ }
+
+ /**
+ * Define Adapter and Loader on create of Activity
+ */
+ @SuppressLint("NewApi")
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mStickyList.setOnItemClickListener(this);
+ mStickyList.setAreHeadersSticky(true);
+ mStickyList.setDrawingListUnderStickyHeader(false);
+ mStickyList.setFastScrollEnabled(true);
+ try {
+ mStickyList.setFastScrollAlwaysVisible(true);
+ } catch (ApiLevelTooLowException e) {
+ }
+
+ /*
+ * ActionBarSherlock does not support MultiChoiceModeListener. Thus multi-selection is only
+ * available for Android >= 3.0
+ */
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ mStickyList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
+ mStickyList.getWrappedList().setMultiChoiceModeListener(new MultiChoiceModeListener() {
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ android.view.MenuInflater inflater = getActivity().getMenuInflater();
+ inflater.inflate(R.menu.key_list_multi, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+
+ // get IDs for checked positions as long array
+ long[] ids;
+
+ switch (item.getItemId()) {
+ case R.id.menu_key_list_multi_encrypt: {
+ ids = mAdapter.getCurrentSelectedMasterKeyIds();
+ encrypt(mode, ids);
+ break;
+ }
+ case R.id.menu_key_list_multi_delete: {
+ ids = mAdapter.getCurrentSelectedMasterKeyIds();
+ showDeleteKeyDialog(mode, ids);
+ break;
+ }
+ case R.id.menu_key_list_multi_export: {
+ ids = mAdapter.getCurrentSelectedMasterKeyIds();
+ ExportHelper mExportHelper = new ExportHelper((ActionBarActivity) getActivity());
+ mExportHelper.showExportKeysDialog(
+ ids, Constants.Path.APP_DIR_FILE, mAdapter.isAnySecretSelected());
+ break;
+ }
+ case R.id.menu_key_list_multi_select_all: {
+ // select all
+ for (int i = 0; i < mStickyList.getCount(); i++) {
+ mStickyList.setItemChecked(i, true);
+ }
+ break;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ mAdapter.clearSelection();
+ }
+
+ @Override
+ public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
+ boolean checked) {
+ if (checked) {
+ mAdapter.setNewSelection(position, checked);
+ } else {
+ mAdapter.removeSelection(position);
+ }
+ int count = mStickyList.getCheckedItemCount();
+ String keysSelected = getResources().getQuantityString(
+ R.plurals.key_list_selected_keys, count, count);
+ mode.setTitle(keysSelected);
+ }
+
+ });
+ }
+
+ // We have a menu item to show in action bar.
+ setHasOptionsMenu(true);
+
+ // NOTE: Not supported by StickyListHeader, but reimplemented here
+ // Start out with a progress indicator.
+ setListShown(false);
+
+ // Create an empty adapter we will use to display the loaded data.
+ mAdapter = new KeyListAdapter(getActivity(), null, Id.type.public_key);
+ mStickyList.setAdapter(mAdapter);
+
+ // Prepare the loader. Either re-connect with an existing one,
+ // or start a new one.
+ getLoaderManager().initLoader(0, null, this);
+ }
+
+ // These are the rows that we will retrieve.
+ static final String[] PROJECTION = new String[]{
+ KeyRings._ID,
+ KeyRings.MASTER_KEY_ID,
+ KeyRings.USER_ID,
+ KeyRings.IS_REVOKED,
+ KeyRings.VERIFIED,
+ KeyRings.HAS_SECRET
+ };
+
+ static final int INDEX_MASTER_KEY_ID = 1;
+ static final int INDEX_USER_ID = 2;
+ static final int INDEX_IS_REVOKED = 3;
+ static final int INDEX_VERIFIED = 4;
+ static final int INDEX_HAS_SECRET = 5;
+
+ @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.buildUnifiedKeyRingsUri();
+ String where = null;
+ String whereArgs[] = null;
+ if (mCurQuery != null) {
+ where = KeyRings.USER_ID + " LIKE ?";
+ whereArgs = new String[]{"%" + mCurQuery + "%"};
+ }
+ // 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, where, whereArgs, null);
+ }
+
+ @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.setSearchQuery(mCurQuery);
+ mAdapter.swapCursor(data);
+
+ mStickyList.setAdapter(mAdapter);
+
+ // this view is made visible if no data is available
+ mStickyList.setEmptyView(getActivity().findViewById(R.id.key_list_empty));
+
+ // NOTE: Not supported by StickyListHeader, but reimplemented here
+ // 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);
+ }
+
+ /**
+ * On click on item, start key view activity
+ */
+ @Override
+ public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
+ Intent viewIntent = null;
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
+ viewIntent = new Intent(getActivity(), ViewKeyActivity.class);
+ } else {
+ viewIntent = new Intent(getActivity(), ViewKeyActivityJB.class);
+ }
+ viewIntent.setData(
+ KeyRings.buildGenericKeyRingUri(Long.toString(mAdapter.getMasterKeyId(position))));
+ startActivity(viewIntent);
+ }
+
+ @TargetApi(11)
+ protected void encrypt(ActionMode mode, long[] masterKeyIds) {
+ Intent intent = new Intent(getActivity(), EncryptActivity.class);
+ intent.setAction(EncryptActivity.ACTION_ENCRYPT);
+ intent.putExtra(EncryptActivity.EXTRA_ENCRYPTION_KEY_IDS, masterKeyIds);
+ // used instead of startActivity set actionbar based on callingPackage
+ startActivityForResult(intent, 0);
+
+ mode.finish();
+ }
+
+ /**
+ * Show dialog to delete key
+ *
+ * @param masterKeyIds
+ */
+ @TargetApi(11)
+ // TODO: this method needs an overhaul to handle both public and secret keys gracefully!
+ public void showDeleteKeyDialog(final ActionMode mode, long[] masterKeyIds) {
+ // Message is received after key is deleted
+ Handler returnHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == DeleteKeyDialogFragment.MESSAGE_OKAY) {
+ mode.finish();
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(returnHandler);
+
+ DeleteKeyDialogFragment deleteKeyDialog = DeleteKeyDialogFragment.newInstance(messenger,
+ masterKeyIds);
+
+ deleteKeyDialog.show(getActivity().getSupportFragmentManager(), "deleteKeyDialog");
+ }
+
+
+ @Override
+ public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
+ // Get the searchview
+ MenuItem searchItem = menu.findItem(R.id.menu_key_list_search);
+ mSearchView = (SearchView) MenuItemCompat.getActionView(searchItem);
+
+ // Execute this when searching
+ mSearchView.setOnQueryTextListener(this);
+
+ // Erase search result without focus
+ MenuItemCompat.setOnActionExpandListener(searchItem, new MenuItemCompat.OnActionExpandListener() {
+ @Override
+ public boolean onMenuItemActionExpand(MenuItem item) {
+ return true;
+ }
+
+ @Override
+ public boolean onMenuItemActionCollapse(MenuItem item) {
+ mCurQuery = null;
+ mSearchView.setQuery("", true);
+ getLoaderManager().restartLoader(0, null, KeyListFragment.this);
+ return true;
+ }
+ });
+
+ super.onCreateOptionsMenu(menu, inflater);
+ }
+
+ @Override
+ public boolean onQueryTextSubmit(String s) {
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String s) {
+ // Called when the action bar search text has changed. Update
+ // the search filter, and restart the loader to do a new query
+ // with this filter.
+ mCurQuery = !TextUtils.isEmpty(s) ? s : null;
+ getLoaderManager().restartLoader(0, null, this);
+ return true;
+ }
+
+ // rebuild functionality of ListFragment, http://stackoverflow.com/a/12504097
+ public void setListShown(boolean shown, boolean animate) {
+ if (mListShown == shown) {
+ return;
+ }
+ mListShown = shown;
+ if (shown) {
+ if (animate) {
+ mProgressContainer.startAnimation(AnimationUtils.loadAnimation(
+ getActivity(), android.R.anim.fade_out));
+ mListContainer.startAnimation(AnimationUtils.loadAnimation(
+ getActivity(), android.R.anim.fade_in));
+ }
+ mProgressContainer.setVisibility(View.GONE);
+ mListContainer.setVisibility(View.VISIBLE);
+ } else {
+ if (animate) {
+ mProgressContainer.startAnimation(AnimationUtils.loadAnimation(
+ getActivity(), android.R.anim.fade_in));
+ mListContainer.startAnimation(AnimationUtils.loadAnimation(
+ getActivity(), android.R.anim.fade_out));
+ }
+ mProgressContainer.setVisibility(View.VISIBLE);
+ mListContainer.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ // rebuild functionality of ListFragment, http://stackoverflow.com/a/12504097
+ public void setListShown(boolean shown) {
+ setListShown(shown, true);
+ }
+
+ // rebuild functionality of ListFragment, http://stackoverflow.com/a/12504097
+ public void setListShownNoAnimation(boolean shown) {
+ setListShown(shown, false);
+ }
+
+ /**
+ * Implements StickyListHeadersAdapter from library
+ */
+ private class KeyListAdapter extends HighlightQueryCursorAdapter implements StickyListHeadersAdapter {
+ private LayoutInflater mInflater;
+
+ private HashMap<Integer, Boolean> mSelection = new HashMap<Integer, Boolean>();
+
+ public KeyListAdapter(Context context, Cursor c, int flags) {
+ super(context, c, flags);
+
+ mInflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public Cursor swapCursor(Cursor newCursor) {
+ return super.swapCursor(newCursor);
+ }
+
+ /**
+ * Bind cursor data to the item list view
+ * <p/>
+ * NOTE: CursorAdapter already implements the ViewHolder pattern in its getView() method.
+ * Thus no ViewHolder is required here.
+ */
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+
+ { // set name and stuff, common to both key types
+ TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId);
+ TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest);
+
+ String userId = cursor.getString(INDEX_USER_ID);
+ String[] userIdSplit = PgpKeyHelper.splitUserId(userId);
+ if (userIdSplit[0] != null) {
+ mainUserId.setText(highlightSearchQuery(userIdSplit[0]));
+ } else {
+ mainUserId.setText(R.string.user_id_no_name);
+ }
+ if (userIdSplit[1] != null) {
+ mainUserIdRest.setText(highlightSearchQuery(userIdSplit[1]));
+ mainUserIdRest.setVisibility(View.VISIBLE);
+ } else {
+ mainUserIdRest.setVisibility(View.GONE);
+ }
+ }
+
+ { // set edit button and revoked info, specific by key type
+ View statusDivider = (View) view.findViewById(R.id.status_divider);
+ FrameLayout statusLayout = (FrameLayout) view.findViewById(R.id.status_layout);
+ Button button = (Button) view.findViewById(R.id.edit);
+ TextView revoked = (TextView) view.findViewById(R.id.revoked);
+ ImageView verified = (ImageView) view.findViewById(R.id.verified);
+
+ if (cursor.getInt(KeyListFragment.INDEX_HAS_SECRET) != 0) {
+ // this is a secret key - show the edit button
+ statusDivider.setVisibility(View.VISIBLE);
+ statusLayout.setVisibility(View.VISIBLE);
+ revoked.setVisibility(View.GONE);
+ verified.setVisibility(View.GONE);
+ button.setVisibility(View.VISIBLE);
+
+ final long id = cursor.getLong(INDEX_MASTER_KEY_ID);
+ button.setOnClickListener(new OnClickListener() {
+ public void onClick(View view) {
+ Intent editIntent = new Intent(getActivity(), EditKeyActivity.class);
+ editIntent.setData(KeyRingData.buildSecretKeyRingUri(Long.toString(id)));
+ editIntent.setAction(EditKeyActivity.ACTION_EDIT_KEY);
+ startActivityForResult(editIntent, 0);
+ }
+ });
+ } else {
+ // this is a public key - hide the edit button, show if it's revoked
+ statusDivider.setVisibility(View.GONE);
+ button.setVisibility(View.GONE);
+
+ boolean isRevoked = cursor.getInt(INDEX_IS_REVOKED) > 0;
+ if(isRevoked) {
+ statusLayout.setVisibility(isRevoked ? View.VISIBLE : View.GONE);
+ revoked.setVisibility(isRevoked ? View.VISIBLE : View.GONE);
+ verified.setVisibility(View.GONE);
+ } else {
+ boolean isVerified = cursor.getInt(INDEX_VERIFIED) > 0;
+ statusLayout.setVisibility(isVerified ? View.VISIBLE : View.GONE);
+ revoked.setVisibility(View.GONE);
+ verified.setVisibility(isVerified ? View.VISIBLE : View.GONE);
+ }
+ }
+ }
+
+ }
+
+ public boolean isSecretAvailable(int id) {
+ if (!mCursor.moveToPosition(id)) {
+ throw new IllegalStateException("couldn't move cursor to position " + id);
+ }
+
+ return mCursor.getInt(INDEX_HAS_SECRET) != 0;
+ }
+ public long getMasterKeyId(int id) {
+ if (!mCursor.moveToPosition(id)) {
+ throw new IllegalStateException("couldn't move cursor to position " + id);
+ }
+
+ return mCursor.getLong(INDEX_MASTER_KEY_ID);
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ return mInflater.inflate(R.layout.key_list_item, parent, false);
+ }
+
+ /**
+ * Creates a new header view and binds the section headers to it. It uses the ViewHolder
+ * pattern. Most functionality is similar to getView() from Android's CursorAdapter.
+ * <p/>
+ * NOTE: The variables mDataValid and mCursor are available due to the super class
+ * CursorAdapter.
+ */
+ @Override
+ public View getHeaderView(int position, View convertView, ViewGroup parent) {
+ HeaderViewHolder holder;
+ if (convertView == null) {
+ holder = new HeaderViewHolder();
+ convertView = mInflater.inflate(R.layout.key_list_header, parent, false);
+ holder.mText = (TextView) convertView.findViewById(R.id.stickylist_header_text);
+ holder.mCount = (TextView) convertView.findViewById(R.id.contacts_num);
+ convertView.setTag(holder);
+ } else {
+ holder = (HeaderViewHolder) convertView.getTag();
+ }
+
+ if (!mDataValid) {
+ // no data available at this point
+ Log.d(Constants.TAG, "getHeaderView: No data available at this point!");
+ return convertView;
+ }
+
+ if (!mCursor.moveToPosition(position)) {
+ throw new IllegalStateException("couldn't move cursor to position " + position);
+ }
+
+ if (mCursor.getInt(KeyListFragment.INDEX_HAS_SECRET) != 0) {
+ { // set contact count
+ int num = mCursor.getCount();
+ String contactsTotal = getResources().getQuantityString(R.plurals.n_contacts, num, num);
+ holder.mCount.setText(contactsTotal);
+ holder.mCount.setVisibility(View.VISIBLE);
+ }
+
+ holder.mText.setText(convertView.getResources().getString(R.string.my_keys));
+ return convertView;
+ }
+
+ // set header text as first char in user id
+ String userId = mCursor.getString(KeyListFragment.INDEX_USER_ID);
+ String headerText = convertView.getResources().getString(R.string.user_id_no_name);
+ if (userId != null && userId.length() > 0) {
+ headerText = "" + userId.subSequence(0, 1).charAt(0);
+ }
+ holder.mText.setText(headerText);
+ holder.mCount.setVisibility(View.GONE);
+ return convertView;
+ }
+
+ /**
+ * Header IDs should be static, position=1 should always return the same Id that is.
+ */
+ @Override
+ public long getHeaderId(int position) {
+ if (!mDataValid) {
+ // no data available at this point
+ Log.d(Constants.TAG, "getHeaderView: No data available at this point!");
+ return -1;
+ }
+
+ if (!mCursor.moveToPosition(position)) {
+ throw new IllegalStateException("couldn't move cursor to position " + position);
+ }
+
+ // early breakout: all secret keys are assigned id 0
+ if (mCursor.getInt(KeyListFragment.INDEX_HAS_SECRET) != 0) {
+ return 1L;
+ }
+ // otherwise, return the first character of the name as ID
+ String userId = mCursor.getString(KeyListFragment.INDEX_USER_ID);
+ if (userId != null && userId.length() > 0) {
+ return userId.charAt(0);
+ } else {
+ return Long.MAX_VALUE;
+ }
+ }
+
+ class HeaderViewHolder {
+ TextView mText;
+ TextView mCount;
+ }
+
+ /**
+ * -------------------------- MULTI-SELECTION METHODS --------------
+ */
+ public void setNewSelection(int position, boolean value) {
+ mSelection.put(position, value);
+ notifyDataSetChanged();
+ }
+
+ public boolean isAnySecretSelected() {
+ for (int pos : mSelection.keySet()) {
+ if(mAdapter.isSecretAvailable(pos))
+ return true;
+ }
+ return false;
+ }
+
+ public long[] getCurrentSelectedMasterKeyIds() {
+ long[] ids = new long[mSelection.size()];
+ int i = 0;
+ // get master key ids
+ for (int pos : mSelection.keySet()) {
+ ids[i++] = mAdapter.getMasterKeyId(pos);
+ }
+ return ids;
+ }
+
+ public void removeSelection(int position) {
+ mSelection.remove(position);
+ notifyDataSetChanged();
+ }
+
+ public void clearSelection() {
+ mSelection.clear();
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ // let the adapter handle setting up the row views
+ View v = super.getView(position, convertView, parent);
+
+ /**
+ * Change color for multi-selection
+ */
+ if (mSelection.get(position) != null) {
+ // selected position color
+ v.setBackgroundColor(parent.getResources().getColor(R.color.emphasis));
+ } else {
+ // default color
+ v.setBackgroundColor(Color.TRANSPARENT);
+ }
+
+ return v;
+ }
+
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PreferencesActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PreferencesActivity.java
new file mode 100644
index 000000000..265bb2139
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PreferencesActivity.java
@@ -0,0 +1,387 @@
+/*
+ * 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 android.annotation.SuppressLint;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceScreen;
+
+import org.spongycastle.bcpg.HashAlgorithmTags;
+import org.spongycastle.openpgp.PGPEncryptedData;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.helper.Preferences;
+import org.sufficientlysecure.keychain.ui.widget.IntegerListPreference;
+
+import java.util.List;
+
+@SuppressLint("NewApi")
+public class PreferencesActivity extends PreferenceActivity {
+
+ public static final String ACTION_PREFS_GEN = "org.sufficientlysecure.keychain.ui.PREFS_GEN";
+ public static final String ACTION_PREFS_ADV = "org.sufficientlysecure.keychain.ui.PREFS_ADV";
+
+ private PreferenceScreen mKeyServerPreference = null;
+ private static Preferences sPreferences;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ sPreferences = Preferences.getPreferences(this);
+ super.onCreate(savedInstanceState);
+
+// final ActionBar actionBar = getSupportActionBar();
+// actionBar.setDisplayShowTitleEnabled(true);
+// actionBar.setDisplayHomeAsUpEnabled(false);
+// actionBar.setHomeButtonEnabled(false);
+
+ String action = getIntent().getAction();
+
+ if (action != null && action.equals(ACTION_PREFS_GEN)) {
+ addPreferencesFromResource(R.xml.gen_preferences);
+
+ initializePassPassphraceCacheTtl(
+ (IntegerListPreference) findPreference(Constants.Pref.PASSPHRASE_CACHE_TTL));
+
+ mKeyServerPreference = (PreferenceScreen) findPreference(Constants.Pref.KEY_SERVERS);
+ String servers[] = sPreferences.getKeyServers();
+ mKeyServerPreference.setSummary(getResources().getQuantityString(R.plurals.n_key_servers,
+ servers.length, 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,
+ sPreferences.getKeyServers());
+ startActivityForResult(intent, Id.request.key_server_preference);
+ return false;
+ }
+ });
+
+ } else if (action != null && action.equals(ACTION_PREFS_ADV)) {
+ addPreferencesFromResource(R.xml.adv_preferences);
+
+ initializeEncryptionAlgorithm(
+ (IntegerListPreference) findPreference(Constants.Pref.DEFAULT_ENCRYPTION_ALGORITHM));
+
+ int[] valueIds = new int[]{Id.choice.compression.none, Id.choice.compression.zip,
+ Id.choice.compression.zlib, Id.choice.compression.bzip2, };
+ String[] entries = new String[]{
+ getString(R.string.choice_none) + " (" + getString(R.string.compression_fast) + ")",
+ "ZIP (" + getString(R.string.compression_fast) + ")",
+ "ZLIB (" + getString(R.string.compression_fast) + ")",
+ "BZIP2 (" + getString(R.string.compression_very_slow) + ")", };
+ String[] values = new String[valueIds.length];
+ for (int i = 0; i < values.length; ++i) {
+ values[i] = "" + valueIds[i];
+ }
+
+ initializeHashAlgorithm(
+ (IntegerListPreference) findPreference(Constants.Pref.DEFAULT_HASH_ALGORITHM),
+ valueIds, entries, values);
+
+ initializeMessageCompression(
+ (IntegerListPreference) findPreference(Constants.Pref.DEFAULT_MESSAGE_COMPRESSION),
+ valueIds, entries, values);
+
+ initializeFileCompression(
+ (IntegerListPreference) findPreference(Constants.Pref.DEFAULT_FILE_COMPRESSION),
+ entries, values);
+
+ initializeAsciiArmor(
+ (CheckBoxPreference) findPreference(Constants.Pref.DEFAULT_ASCII_ARMOR));
+
+ initializeForceV3Signatures(
+ (CheckBoxPreference) findPreference(Constants.Pref.FORCE_V3_SIGNATURES));
+
+ } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
+ // Load the legacy preferences headers
+ addPreferencesFromResource(R.xml.preference_headers_legacy);
+ }
+ }
+
+ @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);
+ sPreferences.setKeyServers(servers);
+ mKeyServerPreference.setSummary(getResources().getQuantityString(
+ R.plurals.n_key_servers, servers.length, servers.length));
+ break;
+ }
+
+ default: {
+ super.onActivityResult(requestCode, resultCode, data);
+ break;
+ }
+ }
+ }
+
+ /* Called only on Honeycomb and later */
+ @Override
+ public void onBuildHeaders(List<Header> target) {
+ super.onBuildHeaders(target);
+ loadHeadersFromResource(R.xml.preference_headers, target);
+ }
+
+ /**
+ * This fragment shows the general preferences in android 3.0+
+ */
+ public static class GeneralPrefsFragment extends PreferenceFragment {
+
+ private PreferenceScreen mKeyServerPreference = null;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.gen_preferences);
+
+ initializePassPassphraceCacheTtl(
+ (IntegerListPreference) findPreference(Constants.Pref.PASSPHRASE_CACHE_TTL));
+
+ mKeyServerPreference = (PreferenceScreen) findPreference(Constants.Pref.KEY_SERVERS);
+ String servers[] = sPreferences.getKeyServers();
+ mKeyServerPreference.setSummary(getResources().getQuantityString(R.plurals.n_key_servers,
+ servers.length, servers.length));
+ mKeyServerPreference
+ .setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ public boolean onPreferenceClick(Preference preference) {
+ Intent intent = new Intent(getActivity(),
+ PreferencesKeyServerActivity.class);
+ intent.putExtra(PreferencesKeyServerActivity.EXTRA_KEY_SERVERS,
+ sPreferences.getKeyServers());
+ startActivityForResult(intent, Id.request.key_server_preference);
+ return false;
+ }
+ });
+ }
+
+ @Override
+ public 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);
+ sPreferences.setKeyServers(servers);
+ mKeyServerPreference.setSummary(getResources().getQuantityString(
+ R.plurals.n_key_servers, servers.length, servers.length));
+ break;
+ }
+
+ default: {
+ super.onActivityResult(requestCode, resultCode, data);
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * This fragment shows the advanced preferences in android 3.0+
+ */
+ public static class AdvancedPrefsFragment extends PreferenceFragment {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.adv_preferences);
+
+ initializeEncryptionAlgorithm(
+ (IntegerListPreference) findPreference(Constants.Pref.DEFAULT_ENCRYPTION_ALGORITHM));
+
+ int[] valueIds = new int[]{Id.choice.compression.none, Id.choice.compression.zip,
+ Id.choice.compression.zlib, Id.choice.compression.bzip2, };
+ String[] entries = new String[]{
+ getString(R.string.choice_none) + " (" + getString(R.string.compression_fast) + ")",
+ "ZIP (" + getString(R.string.compression_fast) + ")",
+ "ZLIB (" + getString(R.string.compression_fast) + ")",
+ "BZIP2 (" + getString(R.string.compression_very_slow) + ")", };
+ String[] values = new String[valueIds.length];
+ for (int i = 0; i < values.length; ++i) {
+ values[i] = "" + valueIds[i];
+ }
+
+ initializeHashAlgorithm(
+ (IntegerListPreference) findPreference(Constants.Pref.DEFAULT_HASH_ALGORITHM),
+ valueIds, entries, values);
+
+ initializeMessageCompression(
+ (IntegerListPreference) findPreference(Constants.Pref.DEFAULT_MESSAGE_COMPRESSION),
+ valueIds, entries, values);
+
+ initializeFileCompression(
+ (IntegerListPreference) findPreference(Constants.Pref.DEFAULT_FILE_COMPRESSION),
+ entries, values);
+
+ initializeAsciiArmor(
+ (CheckBoxPreference) findPreference(Constants.Pref.DEFAULT_ASCII_ARMOR));
+
+ initializeForceV3Signatures(
+ (CheckBoxPreference) findPreference(Constants.Pref.FORCE_V3_SIGNATURES));
+ }
+ }
+
+ protected boolean isValidFragment(String fragmentName) {
+ return AdvancedPrefsFragment.class.getName().equals(fragmentName)
+ || GeneralPrefsFragment.class.getName().equals(fragmentName)
+ || super.isValidFragment(fragmentName);
+ }
+
+ private static void initializePassPassphraceCacheTtl(final IntegerListPreference mPassphraseCacheTtl) {
+ mPassphraseCacheTtl.setValue("" + sPreferences.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());
+ sPreferences.setPassphraseCacheTtl(Integer.parseInt(newValue.toString()));
+ return false;
+ }
+ });
+ }
+
+ private static void initializeEncryptionAlgorithm(final IntegerListPreference mEncryptionAlgorithm) {
+ 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("" + sPreferences.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());
+ sPreferences.setDefaultEncryptionAlgorithm(Integer.parseInt(newValue
+ .toString()));
+ return false;
+ }
+ });
+ }
+
+ private static void initializeHashAlgorithm
+ (final IntegerListPreference mHashAlgorithm, int[] valueIds, String[] entries, String[] values) {
+ 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("" + sPreferences.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());
+ sPreferences.setDefaultHashAlgorithm(Integer.parseInt(newValue.toString()));
+ return false;
+ }
+ });
+ }
+
+ private static void initializeMessageCompression(
+ final IntegerListPreference mMessageCompression,
+ int[] valueIds, String[] entries, String[] values) {
+ mMessageCompression.setEntries(entries);
+ mMessageCompression.setEntryValues(values);
+ mMessageCompression.setValue("" + sPreferences.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());
+ sPreferences.setDefaultMessageCompression(Integer.parseInt(newValue
+ .toString()));
+ return false;
+ }
+ });
+ }
+
+ private static void initializeFileCompression
+ (final IntegerListPreference mFileCompression, String[] entries, String[] values) {
+ mFileCompression.setEntries(entries);
+ mFileCompression.setEntryValues(values);
+ mFileCompression.setValue("" + sPreferences.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());
+ sPreferences.setDefaultFileCompression(Integer.parseInt(newValue.toString()));
+ return false;
+ }
+ });
+ }
+
+ private static void initializeAsciiArmor(final CheckBoxPreference mAsciiArmor) {
+ mAsciiArmor.setChecked(sPreferences.getDefaultAsciiArmor());
+ mAsciiArmor.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ mAsciiArmor.setChecked((Boolean) newValue);
+ sPreferences.setDefaultAsciiArmor((Boolean) newValue);
+ return false;
+ }
+ });
+ }
+
+ private static void initializeForceV3Signatures(final CheckBoxPreference mForceV3Signatures) {
+ mForceV3Signatures.setChecked(sPreferences.getForceV3Signatures());
+ mForceV3Signatures
+ .setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ mForceV3Signatures.setChecked((Boolean) newValue);
+ sPreferences.setForceV3Signatures((Boolean) newValue);
+ return false;
+ }
+ });
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PreferencesKeyServerActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PreferencesKeyServerActivity.java
new file mode 100644
index 000000000..719378274
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PreferencesKeyServerActivity.java
@@ -0,0 +1,130 @@
+/*
+ * 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 android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.ActionBarActivity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.helper.ActionBarHelper;
+import org.sufficientlysecure.keychain.ui.widget.Editor;
+import org.sufficientlysecure.keychain.ui.widget.Editor.EditorListener;
+import org.sufficientlysecure.keychain.ui.widget.KeyServerEditor;
+
+import java.util.Vector;
+
+public class PreferencesKeyServerActivity extends ActionBarActivity implements OnClickListener,
+ EditorListener {
+
+ public static final String EXTRA_KEY_SERVERS = "key_servers";
+
+ private LayoutInflater mInflater;
+ private ViewGroup mEditors;
+ private View mAdd;
+ private TextView mTitle;
+ private TextView mSummary;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Inflate a "Done"/"Cancel" custom action bar view
+ ActionBarHelper.setTwoButtonView(getSupportActionBar(), R.string.btn_okay, R.drawable.ic_action_done,
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // ok
+ okClicked();
+ }
+ }, R.string.btn_do_not_save, R.drawable.ic_action_cancel, new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // cancel
+ cancelClicked();
+ }
+ }
+ );
+
+ setContentView(R.layout.key_server_preference);
+
+ mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ mTitle = (TextView) findViewById(R.id.title);
+ mSummary = (TextView) findViewById(R.id.summary);
+
+ mTitle.setText(R.string.label_key_servers);
+
+ 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 (String serv : servers) {
+ KeyServerEditor view = (KeyServerEditor) mInflater.inflate(
+ R.layout.key_server_editor, mEditors, false);
+ view.setEditorListener(this);
+ view.setValue(serv);
+ mEditors.addView(view);
+ }
+ }
+ }
+
+ public void onDeleted(Editor editor, boolean wasNewItem) {
+ // nothing to do
+ }
+
+ @Override
+ public void onEdited() {
+
+ }
+
+ 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/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectPublicKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectPublicKeyActivity.java
new file mode 100644
index 000000000..874703704
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectPublicKeyActivity.java
@@ -0,0 +1,143 @@
+/*
+ * 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 android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.ActionBarActivity;
+import android.view.View;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.helper.ActionBarHelper;
+
+public class SelectPublicKeyActivity extends ActionBarActivity {
+
+ // Actions for internal use only:
+ public static final String ACTION_SELECT_PUBLIC_KEYS = Constants.INTENT_PREFIX
+ + "SELECT_PUBLIC_KEYRINGS";
+
+ public static final String EXTRA_SELECTED_MASTER_KEY_IDS = "master_key_ids";
+
+ public static final String RESULT_EXTRA_MASTER_KEY_IDS = "master_key_ids";
+ public static final String RESULT_EXTRA_USER_IDS = "user_ids";
+
+ SelectPublicKeyFragment mSelectFragment;
+
+ long mSelectedMasterKeyIds[];
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Inflate a "Done"/"Cancel" custom action bar view
+ ActionBarHelper.setTwoButtonView(getSupportActionBar(), R.string.btn_okay, R.drawable.ic_action_done,
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // ok
+ okClicked();
+ }
+ }, R.string.btn_do_not_save, R.drawable.ic_action_cancel, new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // cancel
+ cancelClicked();
+ }
+ }
+ );
+
+ setContentView(R.layout.select_public_key_activity);
+
+ setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
+
+ handleIntent(getIntent());
+
+ // Check that the activity is using the layout version with
+ // the fragment_container FrameLayout
+ if (findViewById(R.id.select_public_key_fragment_container) != null) {
+
+ // However, if we're being restored from a previous state,
+ // then we don't need to do anything and should return or else
+ // we could end up with overlapping fragments.
+ if (savedInstanceState != null) {
+ return;
+ }
+
+ // Create an instance of the fragment
+ mSelectFragment = SelectPublicKeyFragment.newInstance(mSelectedMasterKeyIds);
+
+ // Add the fragment to the 'fragment_container' FrameLayout
+ getSupportFragmentManager().beginTransaction()
+ .add(R.id.select_public_key_fragment_container, mSelectFragment).commit();
+ }
+
+ // 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());
+ // }
+ // });
+
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ handleIntent(intent);
+ }
+
+ private void handleIntent(Intent intent) {
+ // TODO: reimplement search!
+
+ // 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
+ mSelectedMasterKeyIds = intent.getLongArrayExtra(EXTRA_SELECTED_MASTER_KEY_IDS);
+ }
+
+ 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();
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectPublicKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectPublicKeyFragment.java
new file mode 100644
index 000000000..9bfe3eaa9
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectPublicKeyFragment.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2012-2013 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 android.content.Context;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.compatibility.ListFragmentWorkaround;
+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;
+import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
+import org.sufficientlysecure.keychain.ui.adapter.SelectKeyCursorAdapter;
+
+import java.util.Date;
+import java.util.Vector;
+
+public class SelectPublicKeyFragment extends ListFragmentWorkaround implements TextWatcher,
+ LoaderManager.LoaderCallbacks<Cursor> {
+ public static final String ARG_PRESELECTED_KEY_IDS = "preselected_key_ids";
+
+ private SelectKeyCursorAdapter mAdapter;
+ private EditText mSearchView;
+ private long mSelectedMasterKeyIds[];
+ private String mCurQuery;
+
+ // copied from ListFragment
+ static final int INTERNAL_EMPTY_ID = 0x00ff0001;
+ static final int INTERNAL_PROGRESS_CONTAINER_ID = 0x00ff0002;
+ static final int INTERNAL_LIST_CONTAINER_ID = 0x00ff0003;
+ // added for search view
+ static final int SEARCH_ID = 0x00ff0004;
+
+ /**
+ * Creates new instance of this fragment
+ */
+ public static SelectPublicKeyFragment newInstance(long[] preselectedKeyIds) {
+ SelectPublicKeyFragment frag = new SelectPublicKeyFragment();
+ Bundle args = new Bundle();
+
+ args.putLongArray(ARG_PRESELECTED_KEY_IDS, preselectedKeyIds);
+
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mSelectedMasterKeyIds = getArguments().getLongArray(ARG_PRESELECTED_KEY_IDS);
+ }
+
+ /**
+ * Copied from ListFragment and added EditText for search on top of list.
+ * We do not use a custom layout here, because this breaks the progress bar functionality
+ * of ListFragment.
+ *
+ * @param inflater
+ * @param container
+ * @param savedInstanceState
+ * @return
+ */
+ @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.FILL_PARENT, ViewGroup.LayoutParams.FILL_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.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT));
+
+ // Added for search view: linearLayout, mSearchView
+ LinearLayout linearLayout = new LinearLayout(context);
+ linearLayout.setOrientation(LinearLayout.VERTICAL);
+
+ mSearchView = new EditText(context);
+ mSearchView.setId(SEARCH_ID);
+ mSearchView.setHint(R.string.menu_search);
+ mSearchView.setCompoundDrawablesWithIntrinsicBounds(
+ getResources().getDrawable(R.drawable.ic_action_search), null, null, null);
+
+ linearLayout.addView(mSearchView, new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+
+ ListView lv = new ListView(getActivity());
+ lv.setId(android.R.id.list);
+ lv.setDrawSelectorOnTop(false);
+ linearLayout.addView(lv, new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT));
+
+ lframe.addView(linearLayout, new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT));
+
+ root.addView(lframe, new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT));
+
+ // ------------------------------------------------------------------
+
+ root.setLayoutParams(new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT));
+
+ return root;
+ }
+
+ /**
+ * Define Adapter and Loader on create of Activity
+ */
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ getListView().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.list_empty));
+
+ mSearchView.addTextChangedListener(this);
+
+ mAdapter = new SelectKeyCursorAdapter(getActivity(), null, 0, getListView(), 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 < getListView().getCount(); ++i) {
+ long keyId = mAdapter.getMasterKeyId(i);
+ for (long masterKeyId : masterKeyIds) {
+ if (keyId == masterKeyId) {
+ getListView().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 < getListView().getCount(); ++i) {
+ if (getListView().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 < getListView().getCount(); ++i) {
+ if (getListView().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) {
+ Uri baseUri = KeyRings.buildUnifiedKeyRingsUri();
+
+ // 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(*) FROM " + Tables.KEYS + " AS k"
+ +" WHERE k." + Keys.MASTER_KEY_ID + " = "
+ + KeychainDatabase.Tables.KEYS + "." + Keys.MASTER_KEY_ID
+ + " AND k." + Keys.IS_REVOKED + " = '0'"
+ + " AND k." + Keys.CAN_ENCRYPT + " = '1'"
+ + ") AS " + SelectKeyCursorAdapter.PROJECTION_ROW_AVAILABLE,
+ "(SELECT COUNT(*) FROM " + Tables.KEYS + " AS k"
+ + " WHERE k." + Keys.MASTER_KEY_ID + " = "
+ + KeychainDatabase.Tables.KEYS + "." + Keys.MASTER_KEY_ID
+ + " AND k." + Keys.IS_REVOKED + " = '0'"
+ + " AND k." + Keys.CAN_ENCRYPT + " = '1'"
+ + " AND k." + Keys.CREATION + " <= '" + now + "'"
+ + " AND ( k." + Keys.EXPIRY + " IS NULL OR k." + Keys.EXPIRY + " >= '" + now + "' )"
+ + ") AS " + SelectKeyCursorAdapter.PROJECTION_ROW_VALID, };
+
+ String inMasterKeyList = null;
+ if (mSelectedMasterKeyIds != null && mSelectedMasterKeyIds.length > 0) {
+ inMasterKeyList = Tables.KEYS + "." + KeyRings.MASTER_KEY_ID + " IN (";
+ for (int i = 0; i < mSelectedMasterKeyIds.length; ++i) {
+ if (i != 0) {
+ inMasterKeyList += ", ";
+ }
+ inMasterKeyList += DatabaseUtils.sqlEscapeString("" + mSelectedMasterKeyIds[i]);
+ }
+ inMasterKeyList += ")";
+ }
+
+ String orderBy = UserIds.USER_ID + " ASC";
+ if (inMasterKeyList != null) {
+ // sort by selected master keys
+ orderBy = inMasterKeyList + " DESC, " + orderBy;
+ }
+ String where = null;
+ String whereArgs[] = null;
+ if (mCurQuery != null) {
+ where = UserIds.USER_ID + " LIKE ?";
+ whereArgs = new String[]{"%" + mCurQuery + "%"};
+ }
+
+ // 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, where, whereArgs, 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.setSearchQuery(mCurQuery);
+ 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);
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
+
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ mCurQuery = !TextUtils.isEmpty(editable.toString()) ? editable.toString() : null;
+ getLoaderManager().restartLoader(0, null, this);
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectSecretKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectSecretKeyActivity.java
new file mode 100644
index 000000000..0ff88d97c
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectSecretKeyActivity.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.ActionBarActivity;
+
+import org.sufficientlysecure.keychain.R;
+
+public class SelectSecretKeyActivity extends ActionBarActivity {
+
+ public static final String EXTRA_FILTER_CERTIFY = "filter_certify";
+
+ public static final String RESULT_EXTRA_MASTER_KEY_ID = "master_key_id";
+
+ private boolean mFilterCertify;
+ private SelectSecretKeyFragment mSelectFragment;
+
+ @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);
+
+ mFilterCertify = getIntent().getBooleanExtra(EXTRA_FILTER_CERTIFY, false);
+
+ // Check that the activity is using the layout version with
+ // the fragment_container FrameLayout
+ if (findViewById(R.id.select_secret_key_fragment_container) != null) {
+
+ // However, if we're being restored from a previous state,
+ // then we don't need to do anything and should return or else
+ // we could end up with overlapping fragments.
+ if (savedInstanceState != null) {
+ return;
+ }
+
+ // Create an instance of the fragment
+ mSelectFragment = SelectSecretKeyFragment.newInstance(mFilterCertify);
+
+ // Add the fragment to the 'fragment_container' FrameLayout
+ getSupportFragmentManager().beginTransaction()
+ .add(R.id.select_secret_key_fragment_container, mSelectFragment).commit();
+ }
+ }
+
+ /**
+ * This is executed by SelectSecretKeyFragment after clicking on an item
+ *
+ * @param selectedUri
+ */
+ public void afterListSelection(Uri selectedUri) {
+ Intent data = new Intent();
+ data.setData(selectedUri);
+
+ setResult(RESULT_OK, data);
+ finish();
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectSecretKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectSecretKeyFragment.java
new file mode 100644
index 000000000..9987facbc
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectSecretKeyFragment.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2012-2014 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 android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.ListFragment;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+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;
+import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
+import org.sufficientlysecure.keychain.ui.adapter.SelectKeyCursorAdapter;
+
+import java.util.Date;
+
+public class SelectSecretKeyFragment extends ListFragment implements
+ LoaderManager.LoaderCallbacks<Cursor> {
+
+ private SelectSecretKeyActivity mActivity;
+ private SelectKeyCursorAdapter mAdapter;
+ private ListView mListView;
+
+ private boolean mFilterCertify;
+
+ private static final String ARG_FILTER_CERTIFY = "filter_certify";
+
+ /**
+ * Creates new instance of this fragment
+ */
+ public static SelectSecretKeyFragment newInstance(boolean filterCertify) {
+ SelectSecretKeyFragment frag = new SelectSecretKeyFragment();
+
+ Bundle args = new Bundle();
+ args.putBoolean(ARG_FILTER_CERTIFY, filterCertify);
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mFilterCertify = getArguments().getBoolean(ARG_FILTER_CERTIFY);
+ }
+
+ /**
+ * Define Adapter and Loader on create of Activity
+ */
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mActivity = (SelectSecretKeyActivity) getActivity();
+ mListView = getListView();
+
+ mListView.setOnItemClickListener(new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
+ long masterKeyId = mAdapter.getMasterKeyId(position);
+ Uri result = KeyRings.buildGenericKeyRingUri(String.valueOf(masterKeyId));
+
+ // return data to activity, which results in finishing it
+ mActivity.afterListSelection(result);
+ }
+ });
+
+ // Give some text to display if there is no data. In a real
+ // application this would come from a resource.
+ setEmptyText(getString(R.string.list_empty));
+
+ mAdapter = new SelectKeyCursorAdapter(mActivity, null, 0, mListView, 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.buildUnifiedKeyRingsUri();
+
+ // 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(*) FROM " + Tables.KEYS + " AS k"
+ + " WHERE k." + Keys.MASTER_KEY_ID + " = "
+ + KeychainDatabase.Tables.KEYS + "." + KeyRings.MASTER_KEY_ID
+ + " AND k." + Keys.CAN_CERTIFY + " = '1'"
+ + ") AS cert",
+ "(SELECT COUNT(*) FROM " + Tables.KEYS + " AS k"
+ +" WHERE k." + Keys.MASTER_KEY_ID + " = "
+ + KeychainDatabase.Tables.KEYS + "." + Keys.MASTER_KEY_ID
+ + " AND k." + Keys.IS_REVOKED + " = '0'"
+ + " AND k." + Keys.CAN_SIGN + " = '1'"
+ + ") AS " + SelectKeyCursorAdapter.PROJECTION_ROW_AVAILABLE,
+ "(SELECT COUNT(*) FROM " + Tables.KEYS + " AS k"
+ + " WHERE k." + Keys.MASTER_KEY_ID + " = "
+ + KeychainDatabase.Tables.KEYS + "." + Keys.MASTER_KEY_ID
+ + " AND k." + Keys.IS_REVOKED + " = '0'"
+ + " AND k." + Keys.CAN_SIGN + " = '1'"
+ + " AND k." + Keys.CREATION + " <= '" + now + "'"
+ + " AND ( k." + Keys.EXPIRY + " IS NULL OR k." + Keys.EXPIRY + " >= '" + now + "' )"
+ + ") AS " + SelectKeyCursorAdapter.PROJECTION_ROW_VALID, };
+
+ String orderBy = UserIds.USER_ID + " ASC";
+
+ String where = Tables.KEY_RINGS_SECRET + "." + KeyRings.MASTER_KEY_ID + " IS NOT NULL";
+ if (mFilterCertify) {
+ where += " AND (cert > 0)";
+ }
+
+ // 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, where, 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/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectSecretKeyLayoutFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectSecretKeyLayoutFragment.java
new file mode 100644
index 000000000..514951385
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectSecretKeyLayoutFragment.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.beardedhen.androidbootstrap.BootstrapButton;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
+import org.sufficientlysecure.keychain.provider.KeychainContract;
+
+public class SelectSecretKeyLayoutFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> {
+
+ private TextView mKeyUserId;
+ private TextView mKeyUserIdRest;
+ private TextView mKeyMasterKeyIdHex;
+ private TextView mNoKeySelected;
+ private BootstrapButton mSelectKeyButton;
+ private Boolean mFilterCertify;
+
+ private Uri mReceivedUri = null;
+
+ private SelectSecretKeyCallback mCallback;
+
+ private static final int REQUEST_CODE_SELECT_KEY = 8882;
+
+ private static final int LOADER_ID = 0;
+
+ //The Projection we will retrieve, Master Key ID is for convenience sake,
+ //to avoid having to pass the Key Around
+ final String[] PROJECTION = new String[] {
+ KeychainContract.Keys.MASTER_KEY_ID,
+ KeychainContract.UserIds.USER_ID
+ };
+ final int INDEX_MASTER_KEY_ID = 0;
+ final int INDEX_USER_ID = 1;
+
+ public interface SelectSecretKeyCallback {
+ void onKeySelected(long secretKeyId);
+ }
+
+ public void setCallback(SelectSecretKeyCallback callback) {
+ mCallback = callback;
+ }
+
+ public void setFilterCertify(Boolean filterCertify) {
+ mFilterCertify = filterCertify;
+ }
+
+ public void setNoKeySelected() {
+ mNoKeySelected.setVisibility(View.VISIBLE);
+ mKeyUserId.setVisibility(View.GONE);
+ mKeyUserIdRest.setVisibility(View.GONE);
+ mKeyMasterKeyIdHex.setVisibility(View.GONE);
+ }
+
+ public void setSelectedKeyData(String userName, String email, String masterKeyHex) {
+
+ mNoKeySelected.setVisibility(View.GONE);
+
+ mKeyUserId.setText(userName);
+ mKeyUserIdRest.setText(email);
+ mKeyMasterKeyIdHex.setText(masterKeyHex);
+
+ mKeyUserId.setVisibility(View.VISIBLE);
+ mKeyUserIdRest.setVisibility(View.VISIBLE);
+ mKeyMasterKeyIdHex.setVisibility(View.VISIBLE);
+
+ }
+
+ public void setError(String error) {
+ mNoKeySelected.requestFocus();
+ mNoKeySelected.setError(error);
+ }
+
+ /**
+ * Inflate the layout for this fragment
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.select_secret_key_layout_fragment, container, false);
+
+ mNoKeySelected = (TextView) view.findViewById(R.id.no_key_selected);
+ mKeyUserId = (TextView) view.findViewById(R.id.select_secret_key_user_id);
+ mKeyUserIdRest = (TextView) view.findViewById(R.id.select_secret_key_user_id_rest);
+ mKeyMasterKeyIdHex = (TextView) view.findViewById(R.id.select_secret_key_master_key_hex);
+ mSelectKeyButton = (BootstrapButton) view
+ .findViewById(R.id.select_secret_key_select_key_button);
+ mFilterCertify = false;
+ mSelectKeyButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startSelectKeyActivity();
+ }
+ });
+
+ return view;
+ }
+
+ //For AppSettingsFragment
+ public void selectKey(long masterKeyId) {
+ Uri buildUri = KeychainContract.KeyRings.buildGenericKeyRingUri(String.valueOf(masterKeyId));
+ mReceivedUri = buildUri;
+ getActivity().getSupportLoaderManager().restartLoader(LOADER_ID, null, this);
+ }
+
+ private void startSelectKeyActivity() {
+ Intent intent = new Intent(getActivity(), SelectSecretKeyActivity.class);
+ intent.putExtra(SelectSecretKeyActivity.EXTRA_FILTER_CERTIFY, mFilterCertify);
+ startActivityForResult(intent, REQUEST_CODE_SELECT_KEY);
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ Uri uri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(mReceivedUri);
+ //We don't care about the Loader id
+ return new CursorLoader(getActivity(), uri, PROJECTION, null, null, null);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ if (data.moveToFirst()) {
+ String userName, email, masterKeyHex;
+ String userID = data.getString(INDEX_USER_ID);
+ long masterKeyID = data.getLong(INDEX_MASTER_KEY_ID);
+
+ String splitUserID[] = PgpKeyHelper.splitUserId(userID);
+
+ if (splitUserID[0] != null) {
+ userName = splitUserID[0];
+ } else {
+ userName = getActivity().getResources().getString(R.string.user_id_no_name);
+ }
+
+ if (splitUserID[1] != null) {
+ email = splitUserID[1];
+ } else {
+ email = getActivity().getResources().getString(R.string.error_user_id_no_email);
+ }
+
+ //TODO Can the cursor return invalid values for the Master Key ?
+ masterKeyHex = PgpKeyHelper.convertKeyIdToHexShort(masterKeyID);
+
+ //Set the data
+ setSelectedKeyData(userName, email, masterKeyHex);
+
+ //Give value to the callback
+ mCallback.onKeySelected(masterKeyID);
+ } else {
+ //Set The empty View
+ setNoKeySelected();
+ }
+
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ return;
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case REQUEST_CODE_SELECT_KEY: {
+ if (resultCode == Activity.RESULT_OK) {
+ mReceivedUri = data.getData();
+
+ //Must be restartLoader() or the data will not be updated on selecting a new key
+ getActivity().getSupportLoaderManager().restartLoader(0, null, this);
+
+ mKeyUserId.setError(null);
+ }
+ break;
+ }
+
+ default:
+ super.onActivityResult(requestCode, resultCode, data);
+
+ break;
+ }
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java
new file mode 100644
index 000000000..0e231e6a8
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2012-2014 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 android.app.ProgressDialog;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.Messenger;
+import android.support.v7.app.ActionBarActivity;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.Spinner;
+import android.widget.Toast;
+import com.beardedhen.androidbootstrap.BootstrapButton;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.helper.Preferences;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.util.Log;
+
+/**
+ * Sends the selected public key to a keyserver
+ */
+public class UploadKeyActivity extends ActionBarActivity {
+ private BootstrapButton mUploadButton;
+ private Spinner mKeyServerSpinner;
+
+ private Uri mDataUri;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.key_server_export);
+
+ mUploadButton = (BootstrapButton) findViewById(R.id.btn_export_to_server);
+ mKeyServerSpinner = (Spinner) findViewById(R.id.sign_key_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);
+ mKeyServerSpinner.setAdapter(adapter);
+ if (adapter.getCount() > 0) {
+ mKeyServerSpinner.setSelection(0);
+ } else {
+ mUploadButton.setEnabled(false);
+ }
+
+ mUploadButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ uploadKey();
+ }
+ });
+
+ mDataUri = getIntent().getData();
+ if (mDataUri == null) {
+ Log.e(Constants.TAG, "Intent data missing. Should be Uri of key!");
+ finish();
+ return;
+ }
+ }
+
+ private void uploadKey() {
+ // Send all information needed to service to upload key in other thread
+ Intent intent = new Intent(this, KeychainIntentService.class);
+
+ intent.setAction(KeychainIntentService.ACTION_UPLOAD_KEYRING);
+
+ // set data uri as path to keyring
+ intent.setData(mDataUri);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ String server = (String) mKeyServerSpinner.getSelectedItem();
+ data.putString(KeychainIntentService.UPLOAD_KEY_SERVER, server);
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after uploading is done in KeychainIntentService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
+ getString(R.string.progress_exporting), ProgressDialog.STYLE_HORIZONTAL) {
+ public void handleMessage(Message message) {
+ // handle messages by standard KeychainIntentServiceHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+
+ Toast.makeText(UploadKeyActivity.this, R.string.key_send_success,
+ 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/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewCertActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewCertActivity.java
new file mode 100644
index 000000000..294fadab2
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewCertActivity.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.ActionBarActivity;
+import android.text.format.DateFormat;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.TextView;
+
+import org.spongycastle.bcpg.SignatureSubpacket;
+import org.spongycastle.bcpg.SignatureSubpacketTags;
+import org.spongycastle.bcpg.sig.RevocationReason;
+import org.spongycastle.openpgp.PGPException;
+import org.spongycastle.openpgp.PGPKeyRing;
+import org.spongycastle.openpgp.PGPSignature;
+import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.PgpConversionHelper;
+import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
+import org.sufficientlysecure.keychain.provider.KeychainContract;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.security.SignatureException;
+import java.util.Date;
+
+public class ViewCertActivity extends ActionBarActivity
+ implements LoaderManager.LoaderCallbacks<Cursor> {
+
+ // These are the rows that we will retrieve.
+ static final String[] PROJECTION = new String[]{
+ Certs.MASTER_KEY_ID,
+ Certs.USER_ID,
+ Certs.TYPE,
+ Certs.CREATION,
+ Certs.KEY_ID_CERTIFIER,
+ Certs.SIGNER_UID,
+ Certs.DATA,
+ };
+ private static final int INDEX_MASTER_KEY_ID = 0;
+ private static final int INDEX_USER_ID = 1;
+ private static final int INDEX_TYPE = 2;
+ private static final int INDEX_CREATION = 3;
+ private static final int INDEX_KEY_ID_CERTIFIER = 4;
+ private static final int INDEX_SIGNER_UID = 5;
+ private static final int INDEX_DATA = 6;
+
+ private Uri mDataUri;
+
+ private long mSignerKeyId;
+
+ private TextView mSigneeKey, mSigneeUid, mAlgorithm, mType, mRReason, mCreation;
+ private TextView mSignerKey, mSignerUid, mStatus;
+ private View mRowReason;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ ActionBar actionBar = getSupportActionBar();
+ actionBar.setDisplayHomeAsUpEnabled(true);
+
+ setContentView(R.layout.view_cert_activity);
+
+ mStatus = (TextView) findViewById(R.id.status);
+ mSigneeKey = (TextView) findViewById(R.id.signee_key);
+ mSigneeUid = (TextView) findViewById(R.id.signee_uid);
+ mAlgorithm = (TextView) findViewById(R.id.algorithm);
+ mType = (TextView) findViewById(R.id.signature_type);
+ mRReason = (TextView) findViewById(R.id.reason);
+ mCreation = (TextView) findViewById(R.id.creation);
+
+ mSignerKey = (TextView) findViewById(R.id.signer_key_id);
+ mSignerUid = (TextView) findViewById(R.id.signer_uid);
+
+ mRowReason = findViewById(R.id.row_reason);
+
+ mDataUri = getIntent().getData();
+ if (mDataUri == null) {
+ Log.e(Constants.TAG, "Intent data missing. Should be Uri of key!");
+ finish();
+ return;
+ }
+
+ getSupportLoaderManager().initLoader(0, null, this);
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ // Now create and return a CursorLoader that will take care of
+ // creating a Cursor for the data being displayed.
+ return new CursorLoader(this, mDataUri, PROJECTION, null, null, null);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ if (data.moveToFirst()) {
+ String signeeKey = "0x" + PgpKeyHelper.convertKeyIdToHex(data.getLong(INDEX_MASTER_KEY_ID));
+ mSigneeKey.setText(signeeKey);
+
+ String signeeUid = data.getString(INDEX_USER_ID);
+ mSigneeUid.setText(signeeUid);
+
+ Date creationDate = new Date(data.getLong(INDEX_CREATION) * 1000);
+ mCreation.setText(DateFormat.getDateFormat(getApplicationContext()).format(creationDate));
+
+ mSignerKeyId = data.getLong(INDEX_KEY_ID_CERTIFIER);
+ String signerKey = "0x" + PgpKeyHelper.convertKeyIdToHex(mSignerKeyId);
+ mSignerKey.setText(signerKey);
+
+ String signerUid = data.getString(INDEX_SIGNER_UID);
+ if (signerUid != null) {
+ mSignerUid.setText(signerUid);
+ } else {
+ mSignerUid.setText(R.string.unknown_uid);
+ }
+
+ PGPSignature sig = PgpConversionHelper.BytesToPGPSignature(data.getBlob(INDEX_DATA));
+ PGPKeyRing signeeRing = ProviderHelper.getPGPKeyRing(this,
+ KeychainContract.KeyRingData.buildPublicKeyRingUri(
+ Long.toString(data.getLong(INDEX_MASTER_KEY_ID))));
+ PGPKeyRing signerRing = ProviderHelper.getPGPKeyRing(this,
+ KeychainContract.KeyRingData.buildPublicKeyRingUri(
+ Long.toString(sig.getKeyID())));
+
+ if (signerRing != null) {
+ try {
+ sig.init(new JcaPGPContentVerifierBuilderProvider().setProvider(
+ Constants.BOUNCY_CASTLE_PROVIDER_NAME), signeeRing.getPublicKey());
+ if (sig.verifyCertification(signeeUid, signerRing.getPublicKey())) {
+ mStatus.setText("ok");
+ mStatus.setTextColor(getResources().getColor(R.color.bbutton_success));
+ } else {
+ mStatus.setText("failed!");
+ mStatus.setTextColor(getResources().getColor(R.color.alert));
+ }
+ } catch (SignatureException e) {
+ mStatus.setText("error!");
+ mStatus.setTextColor(getResources().getColor(R.color.alert));
+ } catch (PGPException e) {
+ mStatus.setText("error!");
+ mStatus.setTextColor(getResources().getColor(R.color.alert));
+ }
+ } else {
+ mStatus.setText("key unavailable");
+ mStatus.setTextColor(getResources().getColor(R.color.black));
+ }
+
+ String algorithmStr = PgpKeyHelper.getAlgorithmInfo(sig.getKeyAlgorithm(), 0);
+ mAlgorithm.setText(algorithmStr);
+
+ mRowReason.setVisibility(View.GONE);
+ switch (data.getInt(INDEX_TYPE)) {
+ case PGPSignature.DEFAULT_CERTIFICATION:
+ mType.setText(R.string.cert_default);
+ break;
+ case PGPSignature.NO_CERTIFICATION:
+ mType.setText(R.string.cert_none);
+ break;
+ case PGPSignature.CASUAL_CERTIFICATION:
+ mType.setText(R.string.cert_casual);
+ break;
+ case PGPSignature.POSITIVE_CERTIFICATION:
+ mType.setText(R.string.cert_positive);
+ break;
+ case PGPSignature.CERTIFICATION_REVOCATION: {
+ mType.setText(R.string.cert_revoke);
+ if (sig.getHashedSubPackets().hasSubpacket(SignatureSubpacketTags.REVOCATION_REASON)) {
+ SignatureSubpacket p = sig.getHashedSubPackets().getSubpacket(
+ SignatureSubpacketTags.REVOCATION_REASON);
+ // For some reason, this is missing in SignatureSubpacketInputStream:146
+ if (!(p instanceof RevocationReason)) {
+ p = new RevocationReason(false, p.getData());
+ }
+ String reason = ((RevocationReason) p).getRevocationDescription();
+ mRReason.setText(reason);
+ mRowReason.setVisibility(View.VISIBLE);
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ }
+
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ getMenuInflater().inflate(R.menu.view_cert, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_view_cert_view_signer:
+ // can't do this before the data is initialized
+ Intent viewIntent = null;
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
+ viewIntent = new Intent(this, ViewKeyActivity.class);
+ } else {
+ viewIntent = new Intent(this, ViewKeyActivityJB.class);
+ }
+ //
+ long signerMasterKeyId = ProviderHelper.getMasterKeyId(this,
+ KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(Long.toString(mSignerKeyId))
+ );
+ // TODO notify user of this, maybe offer download?
+ if (mSignerKeyId == 0L)
+ return true;
+ viewIntent.setData(KeyRings.buildGenericKeyRingUri(
+ Long.toString(signerMasterKeyId))
+ );
+ startActivity(viewIntent);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java
new file mode 100644
index 000000000..cce34139c
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2013 Bahtiar 'kalkin' Gadimov
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.support.v4.view.ViewPager;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.ActionBarActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.Window;
+import android.widget.Toast;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.compatibility.ClipboardReflection;
+import org.sufficientlysecure.keychain.helper.ExportHelper;
+import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
+import org.sufficientlysecure.keychain.provider.KeychainContract;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.ui.adapter.TabsAdapter;
+import org.sufficientlysecure.keychain.ui.dialog.ShareNfcDialogFragment;
+import org.sufficientlysecure.keychain.ui.dialog.ShareQrCodeDialogFragment;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class ViewKeyActivity extends ActionBarActivity {
+
+ ExportHelper mExportHelper;
+
+ protected Uri mDataUri;
+
+ public static final String EXTRA_SELECTED_TAB = "selectedTab";
+
+ ViewPager mViewPager;
+ TabsAdapter mTabsAdapter;
+
+ private static final int RESULT_CODE_LOOKUP_KEY = 0x00007006;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+ super.onCreate(savedInstanceState);
+
+ mExportHelper = new ExportHelper(this);
+
+ // let the actionbar look like Android's contact app
+ ActionBar actionBar = getSupportActionBar();
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setIcon(android.R.color.transparent);
+ actionBar.setHomeButtonEnabled(true);
+ actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+
+ setContentView(R.layout.view_key_activity);
+
+ mViewPager = (ViewPager) findViewById(R.id.pager);
+
+ 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);
+ }
+
+ mDataUri = getIntent().getData();
+
+ Bundle mainBundle = new Bundle();
+ mainBundle.putParcelable(ViewKeyMainFragment.ARG_DATA_URI, mDataUri);
+ mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.key_view_tab_main)),
+ ViewKeyMainFragment.class, mainBundle, (selectedTab == 0));
+
+ Bundle certBundle = new Bundle();
+ certBundle.putParcelable(ViewKeyCertsFragment.ARG_DATA_URI, mDataUri);
+ mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.key_view_tab_certs)),
+ ViewKeyCertsFragment.class, certBundle, (selectedTab == 1));
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ getMenuInflater().inflate(R.menu.key_view, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ Intent homeIntent = new Intent(this, KeyListActivity.class);
+ homeIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(homeIntent);
+ return true;
+ case R.id.menu_key_view_update:
+ updateFromKeyserver(mDataUri);
+ return true;
+ case R.id.menu_key_view_export_keyserver:
+ uploadToKeyserver(mDataUri);
+ return true;
+ case R.id.menu_key_view_export_file:
+ exportToFile(mDataUri);
+ return true;
+ case R.id.menu_key_view_share_default_fingerprint:
+ shareKey(mDataUri, true);
+ return true;
+ case R.id.menu_key_view_share_default:
+ shareKey(mDataUri, false);
+ return true;
+ case R.id.menu_key_view_share_qr_code_fingerprint:
+ shareKeyQrCode(mDataUri, true);
+ return true;
+ case R.id.menu_key_view_share_qr_code:
+ shareKeyQrCode(mDataUri, false);
+ return true;
+ case R.id.menu_key_view_share_nfc:
+ shareNfc();
+ return true;
+ case R.id.menu_key_view_share_clipboard:
+ copyToClipboard(mDataUri);
+ return true;
+ case R.id.menu_key_view_delete: {
+ deleteKey(mDataUri);
+ return true;
+ }
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void exportToFile(Uri dataUri) {
+ Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(dataUri);
+
+ HashMap<String, Object> data = ProviderHelper.getGenericData(this,
+ baseUri,
+ new String[]{KeychainContract.Keys.MASTER_KEY_ID, KeychainContract.KeyRings.HAS_SECRET},
+ new int[]{ProviderHelper.FIELD_TYPE_INTEGER, ProviderHelper.FIELD_TYPE_INTEGER});
+
+ mExportHelper.showExportKeysDialog(
+ new long[]{(Long) data.get(KeychainContract.KeyRings.MASTER_KEY_ID)},
+ Constants.Path.APP_DIR_FILE,
+ ((Long) data.get(KeychainContract.KeyRings.HAS_SECRET) == 1)
+ );
+ }
+
+ private void uploadToKeyserver(Uri dataUri) {
+ Intent uploadIntent = new Intent(this, UploadKeyActivity.class);
+ uploadIntent.setData(dataUri);
+ startActivityForResult(uploadIntent, Id.request.export_to_server);
+ }
+
+ private void updateFromKeyserver(Uri dataUri) {
+ byte[] blob = (byte[]) ProviderHelper.getGenericData(
+ this, KeychainContract.KeyRings.buildUnifiedKeyRingUri(dataUri),
+ KeychainContract.Keys.FINGERPRINT, ProviderHelper.FIELD_TYPE_BLOB);
+ String fingerprint = PgpKeyHelper.convertFingerprintToHex(blob);
+
+ Intent queryIntent = new Intent(this, ImportKeysActivity.class);
+ queryIntent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN);
+ queryIntent.putExtra(ImportKeysActivity.EXTRA_FINGERPRINT, fingerprint);
+
+ startActivityForResult(queryIntent, RESULT_CODE_LOOKUP_KEY);
+ }
+
+ private void shareKey(Uri dataUri, boolean fingerprintOnly) {
+ String content;
+ if (fingerprintOnly) {
+ byte[] data = (byte[]) ProviderHelper.getGenericData(
+ this, KeychainContract.KeyRings.buildUnifiedKeyRingUri(dataUri),
+ KeychainContract.Keys.FINGERPRINT, ProviderHelper.FIELD_TYPE_BLOB);
+ if (data != null) {
+ String fingerprint = PgpKeyHelper.convertFingerprintToHex(data);
+ content = Constants.FINGERPRINT_SCHEME + ":" + fingerprint;
+ } else {
+ Toast.makeText(getApplicationContext(), "Bad key selected!",
+ Toast.LENGTH_LONG).show();
+ return;
+ }
+ } else {
+ // get public keyring as ascii armored string
+ long masterKeyId = ProviderHelper.getMasterKeyId(this, dataUri);
+ ArrayList<String> keyringArmored = ProviderHelper.getKeyRingsAsArmoredString(
+ this, new long[]{masterKeyId});
+
+ content = keyringArmored.get(0);
+
+ // Android will fail with android.os.TransactionTooLargeException if key is too big
+ // see http://www.lonestarprod.com/?p=34
+ if (content.length() >= 86389) {
+ Toast.makeText(getApplicationContext(), R.string.key_too_big_for_sharing,
+ Toast.LENGTH_LONG).show();
+ return;
+ }
+ }
+
+ // let user choose application
+ Intent sendIntent = new Intent(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, content);
+ sendIntent.setType("text/plain");
+ startActivity(Intent.createChooser(sendIntent,
+ getResources().getText(R.string.action_share_key_with)));
+ }
+
+ private void shareKeyQrCode(Uri dataUri, boolean fingerprintOnly) {
+ ShareQrCodeDialogFragment dialog = ShareQrCodeDialogFragment.newInstance(dataUri,
+ fingerprintOnly);
+ dialog.show(getSupportFragmentManager(), "shareQrCodeDialog");
+ }
+
+ private void copyToClipboard(Uri dataUri) {
+ // get public keyring as ascii armored string
+ long masterKeyId = ProviderHelper.getMasterKeyId(this, dataUri);
+ ArrayList<String> keyringArmored = ProviderHelper.getKeyRingsAsArmoredString(
+ this, new long[]{masterKeyId});
+
+ ClipboardReflection.copyToClipboard(this, keyringArmored.get(0));
+ Toast.makeText(getApplicationContext(), R.string.key_copied_to_clipboard, Toast.LENGTH_LONG)
+ .show();
+ }
+
+ private void shareNfc() {
+ ShareNfcDialogFragment dialog = ShareNfcDialogFragment.newInstance();
+ dialog.show(getSupportFragmentManager(), "shareNfcDialog");
+ }
+
+ private void deleteKey(Uri dataUri) {
+ // Message is received after key is deleted
+ Handler returnHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ };
+
+ mExportHelper.deleteKey(dataUri, returnHandler);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case RESULT_CODE_LOOKUP_KEY: {
+ if (resultCode == Activity.RESULT_OK) {
+ // TODO: reload key??? move this into fragment?
+ }
+ break;
+ }
+
+ default: {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ break;
+ }
+ }
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivityJB.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivityJB.java
new file mode 100644
index 000000000..6dc0413bb
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivityJB.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.annotation.TargetApi;
+import android.nfc.NdefMessage;
+import android.nfc.NdefRecord;
+import android.nfc.NfcAdapter;
+import android.nfc.NfcAdapter.CreateNdefMessageCallback;
+import android.nfc.NfcAdapter.OnNdefPushCompleteCallback;
+import android.nfc.NfcEvent;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.widget.Toast;
+
+import com.devspark.appmsg.AppMsg;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.io.IOException;
+
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+public class ViewKeyActivityJB extends ViewKeyActivity implements CreateNdefMessageCallback,
+ OnNdefPushCompleteCallback {
+
+ private NfcAdapter mNfcAdapter;
+ private byte[] mSharedKeyringBytes;
+ private static final int NFC_SENT = 1;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ initNfc();
+ }
+
+ /**
+ * NFC: Initialize NFC sharing if OS and device supports it
+ */
+ private void initNfc() {
+ // check if NFC Beam is supported (>= Android 4.1)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ // Check for available NFC Adapter
+ mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
+ if (mNfcAdapter != null) {
+ // init nfc
+ // Register callback to set NDEF message
+ mNfcAdapter.setNdefPushMessageCallback(this, this);
+ // Register callback to listen for message-sent success
+ mNfcAdapter.setOnNdefPushCompleteCallback(this, this);
+ }
+ }
+ }
+
+ /**
+ * NFC: 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.
+ */
+ try {
+ // get public keyring as byte array
+ mSharedKeyringBytes = ProviderHelper.getPGPKeyRing(this, mDataUri).getEncoded();
+
+ NdefMessage msg = new NdefMessage(NdefRecord.createMime(Constants.NFC_MIME,
+ mSharedKeyringBytes), NdefRecord.createApplicationRecord(Constants.PACKAGE_NAME));
+ return msg;
+ } catch(IOException e) {
+ Log.e(Constants.TAG, "Error parsing keyring", e);
+ return null;
+ }
+ }
+
+ /**
+ * NFC: 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
+ mNfcHandler.obtainMessage(NFC_SENT).sendToTarget();
+ }
+
+ /**
+ * NFC: This handler receives a message from onNdefPushComplete
+ */
+ private final Handler mNfcHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case NFC_SENT:
+ AppMsg.makeText(ViewKeyActivityJB.this, R.string.nfc_successfull,
+ AppMsg.STYLE_INFO).show();
+ break;
+ }
+ }
+ };
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyCertsFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyCertsFragment.java
new file mode 100644
index 000000000..b738970f1
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyCertsFragment.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.support.v4.widget.CursorAdapter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.TextView;
+
+import org.spongycastle.openpgp.PGPSignature;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
+import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
+import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
+import org.sufficientlysecure.keychain.util.Log;
+
+import se.emilsjolander.stickylistheaders.ApiLevelTooLowException;
+import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter;
+import se.emilsjolander.stickylistheaders.StickyListHeadersListView;
+
+
+public class ViewKeyCertsFragment extends Fragment
+ implements LoaderManager.LoaderCallbacks<Cursor>, AdapterView.OnItemClickListener {
+
+ // These are the rows that we will retrieve.
+ static final String[] PROJECTION = new String[] {
+ Certs._ID,
+ Certs.MASTER_KEY_ID,
+ Certs.VERIFIED,
+ Certs.TYPE,
+ Certs.RANK,
+ Certs.KEY_ID_CERTIFIER,
+ Certs.USER_ID,
+ Certs.SIGNER_UID
+ };
+
+ // sort by our user id,
+ static final String SORT_ORDER =
+ Tables.CERTS + "." + Certs.RANK + " ASC, "
+ + Certs.VERIFIED + " DESC, " + Certs.TYPE + " DESC, " + Certs.SIGNER_UID + " ASC";
+
+ public static final String ARG_DATA_URI = "data_uri";
+
+ private StickyListHeadersListView mStickyList;
+ private CertListAdapter mAdapter;
+
+ private Uri mDataUri;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.view_key_certs_fragment, container, false);
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mStickyList = (StickyListHeadersListView) getActivity().findViewById(R.id.list);
+
+ if (!getArguments().containsKey(ARG_DATA_URI)) {
+ Log.e(Constants.TAG, "Data missing. Should be Uri of key!");
+ getActivity().finish();
+ return;
+ }
+
+ Uri uri = getArguments().getParcelable(ARG_DATA_URI);
+ mDataUri = Certs.buildCertsUri(uri);
+
+ mStickyList.setAreHeadersSticky(true);
+ mStickyList.setDrawingListUnderStickyHeader(false);
+ mStickyList.setFastScrollEnabled(true);
+ mStickyList.setOnItemClickListener(this);
+
+ try {
+ mStickyList.setFastScrollAlwaysVisible(true);
+ } catch (ApiLevelTooLowException e) {
+ }
+
+ mStickyList.setEmptyView(getActivity().findViewById(R.id.empty));
+
+ // TODO this view is made visible if no data is available
+ // mStickyList.setEmptyView(getActivity().findViewById(R.id.empty));
+
+
+ // Create an empty adapter we will use to display the loaded data.
+ mAdapter = new CertListAdapter(getActivity(), null);
+ mStickyList.setAdapter(mAdapter);
+
+ getLoaderManager().initLoader(0, null, this);
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ // Now create and return a CursorLoader that will take care of
+ // creating a Cursor for the data being displayed.
+ return new CursorLoader(getActivity(), mDataUri, 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.swapCursor(data);
+
+ mStickyList.setAdapter(mAdapter);
+ }
+
+ /**
+ * On click on item, start key view activity
+ */
+ @Override
+ public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
+ if(view.getTag(R.id.tag_mki) != null) {
+ long masterKeyId = (Long) view.getTag(R.id.tag_mki);
+ long rank = (Long) view.getTag(R.id.tag_rank);
+ long certifierId = (Long) view.getTag(R.id.tag_certifierId);
+
+ Intent viewIntent = new Intent(getActivity(), ViewCertActivity.class);
+ viewIntent.setData(Certs.buildCertsSpecificUri(
+ Long.toString(masterKeyId), Long.toString(rank), Long.toString(certifierId)));
+ startActivity(viewIntent);
+ }
+ }
+
+ @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);
+ }
+
+ /**
+ * Implements StickyListHeadersAdapter from library
+ */
+ private class CertListAdapter extends CursorAdapter implements StickyListHeadersAdapter {
+ private LayoutInflater mInflater;
+ private int mIndexMasterKeyId, mIndexUserId, mIndexRank;
+ private int mIndexSignerKeyId, mIndexSignerUserId;
+ private int mIndexVerified, mIndexType;
+
+ public CertListAdapter(Context context, Cursor c) {
+ super(context, c, 0);
+
+ mInflater = LayoutInflater.from(context);
+ initIndex(c);
+ }
+
+ @Override
+ public Cursor swapCursor(Cursor newCursor) {
+ initIndex(newCursor);
+
+ return super.swapCursor(newCursor);
+ }
+
+ /**
+ * Get column indexes for performance reasons just once in constructor and swapCursor. For a
+ * performance comparison see http://stackoverflow.com/a/17999582
+ *
+ * @param cursor
+ */
+ private void initIndex(Cursor cursor) {
+ if (cursor != null) {
+ mIndexMasterKeyId = cursor.getColumnIndexOrThrow(Certs.MASTER_KEY_ID);
+ mIndexUserId = cursor.getColumnIndexOrThrow(Certs.USER_ID);
+ mIndexRank = cursor.getColumnIndexOrThrow(Certs.RANK);
+ mIndexType = cursor.getColumnIndexOrThrow(Certs.TYPE);
+ mIndexVerified = cursor.getColumnIndexOrThrow(Certs.VERIFIED);
+ mIndexSignerKeyId = cursor.getColumnIndexOrThrow(Certs.KEY_ID_CERTIFIER);
+ mIndexSignerUserId = cursor.getColumnIndexOrThrow(Certs.SIGNER_UID);
+ }
+ }
+
+ /**
+ * Bind cursor data to the item list view
+ * <p/>
+ * NOTE: CursorAdapter already implements the ViewHolder pattern in its getView() method.
+ * Thus no ViewHolder is required here.
+ */
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+
+ // set name and stuff, common to both key types
+ TextView wSignerKeyId = (TextView) view.findViewById(R.id.signerKeyId);
+ TextView wSignerUserId = (TextView) view.findViewById(R.id.signerUserId);
+ TextView wSignStatus = (TextView) view.findViewById(R.id.signStatus);
+
+ String signerKeyId = PgpKeyHelper.convertKeyIdToHex(cursor.getLong(mIndexSignerKeyId));
+ String signerUserId = cursor.getString(mIndexSignerUserId);
+ switch(cursor.getInt(mIndexType)) {
+ case PGPSignature.DEFAULT_CERTIFICATION: // 0x10
+ wSignStatus.setText(R.string.cert_default); break;
+ case PGPSignature.NO_CERTIFICATION: // 0x11
+ wSignStatus.setText(R.string.cert_none); break;
+ case PGPSignature.CASUAL_CERTIFICATION: // 0x12
+ wSignStatus.setText(R.string.cert_casual); break;
+ case PGPSignature.POSITIVE_CERTIFICATION: // 0x13
+ wSignStatus.setText(R.string.cert_positive); break;
+ case PGPSignature.CERTIFICATION_REVOCATION: // 0x30
+ wSignStatus.setText(R.string.cert_revoke); break;
+ }
+
+ wSignerUserId.setText(signerUserId);
+ wSignerKeyId.setText(signerKeyId);
+
+ view.setTag(R.id.tag_mki, cursor.getLong(mIndexMasterKeyId));
+ view.setTag(R.id.tag_rank, cursor.getLong(mIndexRank));
+ view.setTag(R.id.tag_certifierId, cursor.getLong(mIndexSignerKeyId));
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ return mInflater.inflate(R.layout.view_key_certs_item, parent, false);
+ }
+
+ /**
+ * Creates a new header view and binds the section headers to it. It uses the ViewHolder
+ * pattern. Most functionality is similar to getView() from Android's CursorAdapter.
+ * <p/>
+ * NOTE: The variables mDataValid and mCursor are available due to the super class
+ * CursorAdapter.
+ */
+ @Override
+ public View getHeaderView(int position, View convertView, ViewGroup parent) {
+ HeaderViewHolder holder;
+ if (convertView == null) {
+ holder = new HeaderViewHolder();
+ convertView = mInflater.inflate(R.layout.view_key_certs_header, parent, false);
+ holder.text = (TextView) convertView.findViewById(R.id.stickylist_header_text);
+ holder.count = (TextView) convertView.findViewById(R.id.certs_num);
+ convertView.setTag(holder);
+ } else {
+ holder = (HeaderViewHolder) convertView.getTag();
+ }
+
+ if (!mDataValid) {
+ // no data available at this point
+ Log.d(Constants.TAG, "getHeaderView: No data available at this point!");
+ return convertView;
+ }
+
+ if (!mCursor.moveToPosition(position)) {
+ throw new IllegalStateException("couldn't move cursor to position " + position);
+ }
+
+ // set header text as first char in user id
+ String userId = mCursor.getString(mIndexUserId);
+ holder.text.setText(userId);
+ holder.count.setVisibility(View.GONE);
+ return convertView;
+ }
+
+ /**
+ * Header IDs should be static, position=1 should always return the same Id that is.
+ */
+ @Override
+ public long getHeaderId(int position) {
+ if (!mDataValid) {
+ // no data available at this point
+ Log.d(Constants.TAG, "getHeaderView: No data available at this point!");
+ return -1;
+ }
+
+ if (!mCursor.moveToPosition(position)) {
+ throw new IllegalStateException("couldn't move cursor to position " + position);
+ }
+
+ // otherwise, return the first character of the name as ID
+ return mCursor.getInt(mIndexRank);
+
+ // sort by the first four characters (should be enough I guess?)
+ // return ByteBuffer.wrap(userId.getBytes()).asLongBuffer().get(0);
+ }
+
+ class HeaderViewHolder {
+ TextView text;
+ TextView count;
+ }
+
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java
new file mode 100644
index 000000000..6e96a338a
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java
@@ -0,0 +1,347 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.text.format.DateFormat;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.beardedhen.androidbootstrap.BootstrapButton;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
+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.ProviderHelper;
+import org.sufficientlysecure.keychain.ui.adapter.ViewKeyKeysAdapter;
+import org.sufficientlysecure.keychain.ui.adapter.ViewKeyUserIdsAdapter;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.util.Date;
+
+
+public class ViewKeyMainFragment extends Fragment implements
+ LoaderManager.LoaderCallbacks<Cursor> {
+
+ public static final String ARG_DATA_URI = "uri";
+
+ private LinearLayout mContainer;
+ private TextView mName;
+ private TextView mEmail;
+ private TextView mComment;
+ private TextView mAlgorithm;
+ private TextView mKeyId;
+ private TextView mExpiry;
+ private TextView mCreation;
+ private TextView mFingerprint;
+ private TextView mSecretKey;
+ private BootstrapButton mActionEdit;
+ private BootstrapButton mActionEncrypt;
+ private BootstrapButton mActionCertify;
+
+ private ListView mUserIds;
+ private ListView mKeys;
+
+ private static final int LOADER_ID_UNIFIED = 0;
+ private static final int LOADER_ID_USER_IDS = 1;
+ private static final int LOADER_ID_KEYS = 2;
+
+ private ViewKeyUserIdsAdapter mUserIdsAdapter;
+ private ViewKeyKeysAdapter mKeysAdapter;
+
+ private Uri mDataUri;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.view_key_main_fragment, container, false);
+
+ mContainer = (LinearLayout) view.findViewById(R.id.container);
+ mName = (TextView) view.findViewById(R.id.name);
+ mEmail = (TextView) view.findViewById(R.id.email);
+ mComment = (TextView) view.findViewById(R.id.comment);
+ mKeyId = (TextView) view.findViewById(R.id.key_id);
+ mAlgorithm = (TextView) view.findViewById(R.id.algorithm);
+ mCreation = (TextView) view.findViewById(R.id.creation);
+ mExpiry = (TextView) view.findViewById(R.id.expiry);
+ mFingerprint = (TextView) view.findViewById(R.id.fingerprint);
+ mSecretKey = (TextView) view.findViewById(R.id.secret_key);
+ mUserIds = (ListView) view.findViewById(R.id.user_ids);
+ mKeys = (ListView) view.findViewById(R.id.keys);
+ mActionEdit = (BootstrapButton) view.findViewById(R.id.action_edit);
+ mActionEncrypt = (BootstrapButton) view.findViewById(R.id.action_encrypt);
+ mActionCertify = (BootstrapButton) view.findViewById(R.id.action_certify);
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ Uri dataUri = getArguments().getParcelable(ARG_DATA_URI);
+ if (dataUri == null) {
+ Log.e(Constants.TAG, "Data missing. Should be Uri of key!");
+ getActivity().finish();
+ return;
+ }
+
+ loadData(dataUri);
+ }
+
+ private void loadData(Uri dataUri) {
+ if (dataUri.equals(mDataUri)) {
+ Log.d(Constants.TAG, "Same URI, no need to load the data again!");
+ return;
+ }
+
+ getActivity().setProgressBarIndeterminateVisibility(Boolean.TRUE);
+ mContainer.setVisibility(View.GONE);
+
+ mDataUri = dataUri;
+
+ Log.i(Constants.TAG, "mDataUri: " + mDataUri.toString());
+
+ mActionEncrypt.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ encryptToContact(mDataUri);
+ }
+ });
+ mActionCertify.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View view) {
+ certifyKey(mDataUri);
+ }
+ });
+
+ mUserIdsAdapter = new ViewKeyUserIdsAdapter(getActivity(), null, 0);
+ mUserIds.setAdapter(mUserIdsAdapter);
+
+ mKeysAdapter = new ViewKeyKeysAdapter(getActivity(), null, 0);
+ mKeys.setAdapter(mKeysAdapter);
+
+ // Prepare the loaders. Either re-connect with an existing ones,
+ // or start new ones.
+ getActivity().getSupportLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this);
+ getActivity().getSupportLoaderManager().initLoader(LOADER_ID_USER_IDS, null, this);
+ getActivity().getSupportLoaderManager().initLoader(LOADER_ID_KEYS, null, this);
+ }
+
+ static final String[] UNIFIED_PROJECTION = new String[] {
+ KeyRings._ID, KeyRings.MASTER_KEY_ID, KeyRings.HAS_SECRET,
+ KeyRings.USER_ID, KeyRings.FINGERPRINT,
+ KeyRings.ALGORITHM, KeyRings.KEY_SIZE, KeyRings.CREATION, KeyRings.EXPIRY,
+
+ };
+ static final int INDEX_UNIFIED_MKI = 1;
+ static final int INDEX_UNIFIED_HAS_SECRET = 2;
+ static final int INDEX_UNIFIED_UID = 3;
+ static final int INDEX_UNIFIED_FINGERPRINT = 4;
+ static final int INDEX_UNIFIED_ALGORITHM = 5;
+ static final int INDEX_UNIFIED_KEY_SIZE = 6;
+ static final int INDEX_UNIFIED_CREATION = 7;
+ static final int INDEX_UNIFIED_EXPIRY = 8;
+
+ static final String[] KEYS_PROJECTION = new String[] {
+ Keys._ID,
+ Keys.KEY_ID, Keys.RANK, Keys.ALGORITHM, Keys.KEY_SIZE,
+ Keys.CAN_CERTIFY, Keys.CAN_ENCRYPT, Keys.CAN_SIGN, Keys.IS_REVOKED,
+ Keys.CREATION, Keys.EXPIRY, Keys.FINGERPRINT
+ };
+ static final int KEYS_INDEX_CAN_ENCRYPT = 6;
+
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ switch (id) {
+ case LOADER_ID_UNIFIED: {
+ Uri baseUri = KeyRings.buildUnifiedKeyRingUri(mDataUri);
+ return new CursorLoader(getActivity(), baseUri, UNIFIED_PROJECTION, null, null, null);
+ }
+ case LOADER_ID_USER_IDS: {
+ Uri baseUri = UserIds.buildUserIdsUri(mDataUri);
+ return new CursorLoader(getActivity(), baseUri, ViewKeyUserIdsAdapter.USER_IDS_PROJECTION, null, null, null);
+ }
+ case LOADER_ID_KEYS: {
+ Uri baseUri = Keys.buildKeysUri(mDataUri);
+ return new CursorLoader(getActivity(), baseUri, KEYS_PROJECTION, null, null, null);
+ }
+
+ default:
+ return null;
+ }
+ }
+
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ /* TODO better error handling? May cause problems when a key is deleted,
+ * because the notification triggers faster than the activity closes.
+ */
+ // Avoid NullPointerExceptions...
+ if(data.getCount() == 0)
+ return;
+ // Swap the new cursor in. (The framework will take care of closing the
+ // old cursor once we return.)
+ switch (loader.getId()) {
+ case LOADER_ID_UNIFIED: {
+ if (data.moveToFirst()) {
+ // get name, email, and comment from USER_ID
+ String[] mainUserId = PgpKeyHelper.splitUserId(data.getString(INDEX_UNIFIED_UID));
+ if (mainUserId[0] != null) {
+ getActivity().setTitle(mainUserId[0]);
+ mName.setText(mainUserId[0]);
+ } else {
+ getActivity().setTitle(R.string.user_id_no_name);
+ mName.setText(R.string.user_id_no_name);
+ }
+ mEmail.setText(mainUserId[1]);
+ mComment.setText(mainUserId[2]);
+
+ if (data.getInt(INDEX_UNIFIED_HAS_SECRET) != 0) {
+ mSecretKey.setTextColor(getResources().getColor(R.color.emphasis));
+ mSecretKey.setText(R.string.secret_key_yes);
+
+ // edit button
+ mActionEdit.setVisibility(View.VISIBLE);
+ mActionEdit.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View view) {
+ Intent editIntent = new Intent(getActivity(), EditKeyActivity.class);
+ editIntent.setData(mDataUri);
+ editIntent.setAction(EditKeyActivity.ACTION_EDIT_KEY);
+ startActivityForResult(editIntent, 0);
+ }
+ });
+ } else {
+ mSecretKey.setTextColor(Color.BLACK);
+ mSecretKey.setText(getResources().getString(R.string.secret_key_no));
+
+ // certify button
+ mActionCertify.setVisibility(View.VISIBLE);
+ // edit button
+ mActionEdit.setVisibility(View.GONE);
+ }
+
+ // get key id from MASTER_KEY_ID
+ long masterKeyId = data.getLong(INDEX_UNIFIED_MKI);
+ String keyIdStr = PgpKeyHelper.convertKeyIdToHex(masterKeyId);
+ mKeyId.setText(keyIdStr);
+
+ // get creation date from CREATION
+ if (data.isNull(INDEX_UNIFIED_CREATION)) {
+ mCreation.setText(R.string.none);
+ } else {
+ Date creationDate = new Date(data.getLong(INDEX_UNIFIED_CREATION) * 1000);
+
+ mCreation.setText(
+ DateFormat.getDateFormat(getActivity().getApplicationContext()).format(
+ creationDate));
+ }
+
+ // get expiry date from EXPIRY
+ if (data.isNull(INDEX_UNIFIED_EXPIRY)) {
+ mExpiry.setText(R.string.none);
+ } else {
+ Date expiryDate = new Date(data.getLong(INDEX_UNIFIED_EXPIRY) * 1000);
+
+ mExpiry.setText(
+ DateFormat.getDateFormat(getActivity().getApplicationContext()).format(
+ expiryDate));
+ }
+
+ String algorithmStr = PgpKeyHelper.getAlgorithmInfo(
+ data.getInt(INDEX_UNIFIED_ALGORITHM), data.getInt(INDEX_UNIFIED_KEY_SIZE));
+ mAlgorithm.setText(algorithmStr);
+
+ byte[] fingerprintBlob = data.getBlob(INDEX_UNIFIED_FINGERPRINT);
+ String fingerprint = PgpKeyHelper.convertFingerprintToHex(fingerprintBlob);
+ mFingerprint.setText(PgpKeyHelper.colorizeFingerprint(fingerprint));
+
+ break;
+ }
+ }
+
+ case LOADER_ID_USER_IDS:
+ mUserIdsAdapter.swapCursor(data);
+ break;
+
+ case LOADER_ID_KEYS:
+ // hide encrypt button if no encryption key is available
+ boolean canEncrypt = false;
+ data.moveToFirst();
+ do {
+ if (data.getInt(KEYS_INDEX_CAN_ENCRYPT) == 1) {
+ canEncrypt = true;
+ break;
+ }
+ } while (data.moveToNext());
+ if (!canEncrypt) {
+ mActionEncrypt.setVisibility(View.GONE);
+ }
+
+ mKeysAdapter.swapCursor(data);
+ break;
+ }
+ getActivity().setProgressBarIndeterminateVisibility(Boolean.FALSE);
+ mContainer.setVisibility(View.VISIBLE);
+ }
+
+ /**
+ * 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.
+ */
+ public void onLoaderReset(Loader<Cursor> loader) {
+ switch (loader.getId()) {
+ case LOADER_ID_USER_IDS:
+ mUserIdsAdapter.swapCursor(null);
+ break;
+ case LOADER_ID_KEYS:
+ mKeysAdapter.swapCursor(null);
+ break;
+ }
+ }
+
+ private void encryptToContact(Uri dataUri) {
+ // TODO preselect from uri? should be feasible without trivial query
+ long keyId = ProviderHelper.getMasterKeyId(getActivity(), dataUri);
+
+ long[] encryptionKeyIds = new long[]{ keyId };
+ Intent intent = new Intent(getActivity(), EncryptActivity.class);
+ intent.setAction(EncryptActivity.ACTION_ENCRYPT);
+ intent.putExtra(EncryptActivity.EXTRA_ENCRYPTION_KEY_IDS, encryptionKeyIds);
+ // used instead of startActivity set actionbar based on callingPackage
+ startActivityForResult(intent, 0);
+ }
+
+ private void certifyKey(Uri dataUri) {
+ Intent signIntent = new Intent(getActivity(), CertifyKeyActivity.class);
+ signIntent.setData(dataUri);
+ startActivity(signIntent);
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/AsyncTaskResultWrapper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/AsyncTaskResultWrapper.java
new file mode 100644
index 000000000..5f2aec4fe
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/AsyncTaskResultWrapper.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.adapter;
+
+/**
+ * The AsyncTaskResultWrapper is used to wrap a result from a AsyncTask (for example: Loader).
+ * You can pass the result and an exception in it if an error occurred.
+ * Concept found at:
+ * https://stackoverflow.com/questions/19593577/how-to-handle-errors-in-custom-asynctaskloader
+ *
+ * @param <T> - Typ of the result which is wrapped
+ */
+public class AsyncTaskResultWrapper<T> {
+
+ private final T mResult;
+ private final Exception mError;
+
+ public AsyncTaskResultWrapper(T result, Exception error) {
+ this.mResult = result;
+ this.mError = error;
+ }
+
+ public T getResult() {
+ return mResult;
+ }
+
+ public Exception getError() {
+ return mError;
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/HighlightQueryCursorAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/HighlightQueryCursorAdapter.java
new file mode 100644
index 000000000..a3ed08a4c
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/HighlightQueryCursorAdapter.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.adapter;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.support.v4.widget.CursorAdapter;
+import android.text.Spannable;
+import android.text.style.ForegroundColorSpan;
+import org.sufficientlysecure.keychain.R;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public abstract class HighlightQueryCursorAdapter extends CursorAdapter {
+
+ private String mCurQuery;
+
+ public HighlightQueryCursorAdapter(Context context, Cursor c, int flags) {
+ super(context, c, flags);
+ mCurQuery = null;
+ }
+
+ public void setSearchQuery(String searchQuery) {
+ mCurQuery = searchQuery;
+ }
+
+ public String getSearchQuery() {
+ return mCurQuery;
+ }
+
+ protected Spannable highlightSearchQuery(String text) {
+ Spannable highlight = Spannable.Factory.getInstance().newSpannable(text);
+
+ if (mCurQuery != null) {
+ Pattern pattern = Pattern.compile("(?i)" + mCurQuery);
+ Matcher matcher = pattern.matcher(text);
+ if (matcher.find()) {
+ highlight.setSpan(
+ new ForegroundColorSpan(mContext.getResources().getColor(R.color.emphasis)),
+ matcher.start(),
+ matcher.end(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ return highlight;
+ } else {
+ return highlight;
+ }
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java
new file mode 100644
index 000000000..f322ea980
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.adapter;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Color;
+import android.os.Build;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.LinearLayout;
+import android.widget.LinearLayout.LayoutParams;
+import android.widget.TextView;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ImportKeysAdapter extends ArrayAdapter<ImportKeysListEntry> {
+ protected LayoutInflater mInflater;
+ protected Activity mActivity;
+
+ protected List<ImportKeysListEntry> mData;
+
+ static class ViewHolder {
+ public TextView mainUserId;
+ public TextView mainUserIdRest;
+ public TextView keyId;
+ public TextView fingerprint;
+ public TextView algorithm;
+ public TextView status;
+ }
+
+ public ImportKeysAdapter(Activity activity) {
+ super(activity, -1);
+ mActivity = activity;
+ mInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ @SuppressLint("NewApi")
+ public void setData(List<ImportKeysListEntry> data) {
+ clear();
+ if (data != null) {
+ this.mData = data;
+
+ // add data to extended ArrayAdapter
+ if (Build.VERSION.SDK_INT >= 11) {
+ addAll(data);
+ } else {
+ for (ImportKeysListEntry entry : data) {
+ add(entry);
+ }
+ }
+ }
+ }
+
+ public List<ImportKeysListEntry> getData() {
+ return mData;
+ }
+
+ public ArrayList<ImportKeysListEntry> getSelectedData() {
+ ArrayList<ImportKeysListEntry> selectedData = new ArrayList<ImportKeysListEntry>();
+ for (ImportKeysListEntry entry : mData) {
+ if (entry.isSelected()) {
+ selectedData.add(entry);
+ }
+ }
+ return selectedData;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ImportKeysListEntry entry = mData.get(position);
+ ViewHolder holder;
+ if (convertView == null) {
+ holder = new ViewHolder();
+ convertView = mInflater.inflate(R.layout.import_keys_list_entry, null);
+ holder.mainUserId = (TextView) convertView.findViewById(R.id.mainUserId);
+ holder.mainUserIdRest = (TextView) convertView.findViewById(R.id.mainUserIdRest);
+ holder.keyId = (TextView) convertView.findViewById(R.id.keyId);
+ holder.fingerprint = (TextView) convertView.findViewById(R.id.fingerprint);
+ holder.algorithm = (TextView) convertView.findViewById(R.id.algorithm);
+ holder.status = (TextView) convertView.findViewById(R.id.status);
+ convertView.setTag(holder);
+ } else {
+ holder = (ViewHolder) convertView.getTag();
+ }
+ // main user id
+ String userId = entry.userIds.get(0);
+ String[] userIdSplit = PgpKeyHelper.splitUserId(userId);
+
+ // name
+ if (userIdSplit[0] != null) {
+ // show red user id if it is a secret key
+ if (entry.secretKey) {
+ userIdSplit[0] = mActivity.getString(R.string.secret_key) + " " + userIdSplit[0];
+ holder.mainUserId.setTextColor(Color.RED);
+ }
+ holder.mainUserId.setText(userIdSplit[0]);
+ } else {
+ holder.mainUserId.setText(R.string.user_id_no_name);
+ }
+
+ // email
+ if (userIdSplit[1] != null) {
+ holder.mainUserIdRest.setText(userIdSplit[1]);
+ holder.mainUserIdRest.setVisibility(View.VISIBLE);
+ } else {
+ holder.mainUserIdRest.setVisibility(View.GONE);
+ }
+
+ holder.keyId.setText(entry.keyIdHex);
+
+ if (entry.fingerPrintHex != null) {
+ holder.fingerprint.setText(PgpKeyHelper.colorizeFingerprint(entry.fingerPrintHex));
+ holder.fingerprint.setVisibility(View.VISIBLE);
+ } else {
+ holder.fingerprint.setVisibility(View.GONE);
+ }
+
+ holder.algorithm.setText("" + entry.bitStrength + "/" + entry.algorithm);
+
+ if (entry.revoked) {
+ holder.status.setText(R.string.revoked);
+ } else {
+ holder.status.setVisibility(View.GONE);
+ }
+
+ LinearLayout ll = (LinearLayout) convertView.findViewById(R.id.list);
+ ll.removeAllViews();
+ if (entry.userIds.size() == 1) {
+ ll.setVisibility(View.GONE);
+ } else {
+ boolean first = true;
+ boolean second = true;
+ for (String uid : entry.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.import_keys_list_entry_user_id, null);
+ uidView.setText(uid);
+ ll.addView(uidView);
+ second = false;
+ }
+ }
+
+ CheckBox cBox = (CheckBox) convertView.findViewById(R.id.selected);
+ cBox.setChecked(entry.isSelected());
+
+ return convertView;
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListEntry.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListEntry.java
new file mode 100644
index 000000000..5631d40ea
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListEntry.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.adapter;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.SparseArray;
+
+import org.spongycastle.openpgp.PGPKeyRing;
+import org.spongycastle.openpgp.PGPPublicKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
+import org.sufficientlysecure.keychain.util.IterableIterator;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Date;
+
+public class ImportKeysListEntry implements Serializable, Parcelable {
+ private static final long serialVersionUID = -7797972103284992662L;
+
+ public ArrayList<String> userIds;
+ public long keyId;
+ public String keyIdHex;
+ public boolean revoked;
+ public Date date; // TODO: not displayed
+ public String fingerPrintHex;
+ public int bitStrength;
+ public String algorithm;
+ public boolean secretKey;
+
+ private boolean mSelected;
+
+ private byte[] mBytes = new byte[]{};
+
+ public ImportKeysListEntry(ImportKeysListEntry b) {
+ this.userIds = b.userIds;
+ this.keyId = b.keyId;
+ this.revoked = b.revoked;
+ this.date = b.date;
+ this.fingerPrintHex = b.fingerPrintHex;
+ this.keyIdHex = b.keyIdHex;
+ this.bitStrength = b.bitStrength;
+ this.algorithm = b.algorithm;
+ this.secretKey = b.secretKey;
+ this.mSelected = b.mSelected;
+ this.mBytes = b.mBytes;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeStringList(userIds);
+ dest.writeLong(keyId);
+ dest.writeByte((byte) (revoked ? 1 : 0));
+ dest.writeSerializable(date);
+ dest.writeString(fingerPrintHex);
+ dest.writeString(keyIdHex);
+ dest.writeInt(bitStrength);
+ dest.writeString(algorithm);
+ dest.writeByte((byte) (secretKey ? 1 : 0));
+ dest.writeByte((byte) (mSelected ? 1 : 0));
+ dest.writeInt(mBytes.length);
+ dest.writeByteArray(mBytes);
+ }
+
+ public static final Creator<ImportKeysListEntry> CREATOR = new Creator<ImportKeysListEntry>() {
+ public ImportKeysListEntry createFromParcel(final Parcel source) {
+ ImportKeysListEntry vr = new ImportKeysListEntry();
+ vr.userIds = new ArrayList<String>();
+ source.readStringList(vr.userIds);
+ vr.keyId = source.readLong();
+ vr.revoked = source.readByte() == 1;
+ vr.date = (Date) source.readSerializable();
+ vr.fingerPrintHex = source.readString();
+ vr.keyIdHex = source.readString();
+ vr.bitStrength = source.readInt();
+ vr.algorithm = source.readString();
+ vr.secretKey = source.readByte() == 1;
+ vr.mSelected = source.readByte() == 1;
+ vr.mBytes = new byte[source.readInt()];
+ source.readByteArray(vr.mBytes);
+
+ return vr;
+ }
+
+ public ImportKeysListEntry[] newArray(final int size) {
+ return new ImportKeysListEntry[size];
+ }
+ };
+
+ public String getKeyIdHex() {
+ return keyIdHex;
+ }
+
+ public byte[] getBytes() {
+ return mBytes;
+ }
+
+ public void setBytes(byte[] bytes) {
+ this.mBytes = bytes;
+ }
+
+ public boolean isSelected() {
+ return mSelected;
+ }
+
+ public void setSelected(boolean selected) {
+ this.mSelected = selected;
+ }
+
+ public long getKeyId() {
+ return keyId;
+ }
+
+ public void setKeyId(long keyId) {
+ this.keyId = keyId;
+ }
+
+ public void setKeyIdHex(String keyIdHex) {
+ this.keyIdHex = keyIdHex;
+ }
+
+ public boolean isRevoked() {
+ return revoked;
+ }
+
+ public void setRevoked(boolean revoked) {
+ this.revoked = revoked;
+ }
+
+ public Date getDate() {
+ return date;
+ }
+
+ public void setDate(Date date) {
+ this.date = date;
+ }
+
+ public String getFingerPrintHex() {
+ return fingerPrintHex;
+ }
+
+ public void setFingerPrintHex(String fingerPrintHex) {
+ this.fingerPrintHex = fingerPrintHex;
+ }
+
+ public int getBitStrength() {
+ return bitStrength;
+ }
+
+ public void setBitStrength(int bitStrength) {
+ this.bitStrength = bitStrength;
+ }
+
+ public String getAlgorithm() {
+ return algorithm;
+ }
+
+ public void setAlgorithm(String algorithm) {
+ this.algorithm = algorithm;
+ }
+
+ public boolean isSecretKey() {
+ return secretKey;
+ }
+
+ public void setSecretKey(boolean secretKey) {
+ this.secretKey = secretKey;
+ }
+
+ public ArrayList<String> getUserIds() {
+ return userIds;
+ }
+
+ public void setUserIds(ArrayList<String> userIds) {
+ this.userIds = userIds;
+ }
+
+ /**
+ * Constructor for later querying from keyserver
+ */
+ public ImportKeysListEntry() {
+ // keys from keyserver are always public keys
+ secretKey = false;
+ // do not select by default
+ mSelected = false;
+ userIds = new ArrayList<String>();
+ }
+
+ /**
+ * Constructor based on key object, used for import from NFC, QR Codes, files
+ */
+ @SuppressWarnings("unchecked")
+ public ImportKeysListEntry(PGPKeyRing pgpKeyRing) {
+ // save actual key object into entry, used to import it later
+ try {
+ this.mBytes = pgpKeyRing.getEncoded();
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "IOException on pgpKeyRing.getEncoded()", e);
+ }
+
+ // selected is default
+ this.mSelected = true;
+
+ if (pgpKeyRing instanceof PGPSecretKeyRing) {
+ secretKey = true;
+ } else {
+ secretKey = false;
+ }
+
+ userIds = new ArrayList<String>();
+ for (String userId : new IterableIterator<String>(pgpKeyRing.getPublicKey().getUserIDs())) {
+ userIds.add(userId);
+ }
+
+ this.keyId = pgpKeyRing.getPublicKey().getKeyID();
+ this.keyIdHex = PgpKeyHelper.convertKeyIdToHex(keyId);
+
+ this.revoked = pgpKeyRing.getPublicKey().isRevoked();
+ this.fingerPrintHex = PgpKeyHelper.convertFingerprintToHex(pgpKeyRing.getPublicKey()
+ .getFingerprint());
+ this.bitStrength = pgpKeyRing.getPublicKey().getBitStrength();
+ final int algorithm = pgpKeyRing.getPublicKey().getAlgorithm();
+ this.algorithm = getAlgorithmFromId(algorithm);
+ }
+
+ /**
+ * Based on <a href="http://tools.ietf.org/html/rfc2440#section-9.1">OpenPGP Message Format</a>
+ */
+ private static final SparseArray<String> ALGORITHM_IDS = new SparseArray<String>() {{
+ put(-1, "unknown"); // TODO: with resources
+ put(0, "unencrypted");
+ put(PGPPublicKey.RSA_GENERAL, "RSA");
+ put(PGPPublicKey.RSA_ENCRYPT, "RSA");
+ put(PGPPublicKey.RSA_SIGN, "RSA");
+ put(PGPPublicKey.ELGAMAL_ENCRYPT, "ElGamal");
+ put(PGPPublicKey.ELGAMAL_GENERAL, "ElGamal");
+ put(PGPPublicKey.DSA, "DSA");
+ put(PGPPublicKey.EC, "ECC");
+ put(PGPPublicKey.ECDSA, "ECC");
+ put(PGPPublicKey.ECDH, "ECC");
+ }};
+
+ /**
+ * Based on <a href="http://tools.ietf.org/html/rfc2440#section-9.1">OpenPGP Message Format</a>
+ */
+ public static String getAlgorithmFromId(int algorithmId) {
+ return (ALGORITHM_IDS.get(algorithmId) != null ?
+ ALGORITHM_IDS.get(algorithmId) :
+ ALGORITHM_IDS.get(-1));
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListLoader.java
new file mode 100644
index 000000000..c9983213c
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListLoader.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.adapter;
+
+import android.content.Context;
+import android.support.v4.content.AsyncTaskLoader;
+import org.spongycastle.openpgp.PGPKeyRing;
+import org.spongycastle.openpgp.PGPObjectFactory;
+import org.spongycastle.openpgp.PGPUtil;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.util.InputData;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.util.PositionAwareInputStream;
+
+import java.io.BufferedInputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+public class ImportKeysListLoader
+ extends AsyncTaskLoader<AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>> {
+
+ public static class FileHasNoContent extends Exception {
+
+ }
+
+ public static class NonPgpPart extends Exception {
+ private int mCount;
+
+ public NonPgpPart(int count) {
+ this.mCount = count;
+ }
+
+ public int getCount() {
+ return mCount;
+ }
+ }
+
+ Context mContext;
+
+ InputData mInputData;
+
+ ArrayList<ImportKeysListEntry> mData = new ArrayList<ImportKeysListEntry>();
+ AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>> mEntryListWrapper;
+
+ public ImportKeysListLoader(Context context, InputData inputData) {
+ super(context);
+ this.mContext = context;
+ this.mInputData = inputData;
+ }
+
+ @Override
+ public AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>> loadInBackground() {
+
+ mEntryListWrapper = new AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>(mData, null);
+
+ if (mInputData == null) {
+ Log.e(Constants.TAG, "Input data is null!");
+ return mEntryListWrapper;
+ }
+
+ generateListOfKeyrings(mInputData);
+
+ return mEntryListWrapper;
+ }
+
+ @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(AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>> data) {
+ super.deliverResult(data);
+ }
+
+ /**
+ * Reads all PGPKeyRing objects from input
+ *
+ * @param inputData
+ * @return
+ */
+ private void generateListOfKeyrings(InputData inputData) {
+
+ boolean isEmpty = true;
+ int nonPgpCounter = 0;
+
+ 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
+ // armor blocks
+ BufferedInputStream bufferedInput = new BufferedInputStream(progressIn);
+ try {
+
+ // read all available blocks... (asc files can contain many blocks with BEGIN END)
+ while (bufferedInput.available() > 0) {
+ isEmpty = false;
+ 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!");
+ nonPgpCounter++;
+ }
+ }
+ }
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "Exception on parsing key file!", e);
+ mEntryListWrapper = new AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>(mData, e);
+ nonPgpCounter = 0;
+ }
+
+ if (isEmpty) {
+ Log.e(Constants.TAG, "File has no content!", new FileHasNoContent());
+ mEntryListWrapper = new AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>
+ (mData, new FileHasNoContent());
+ }
+
+ if (nonPgpCounter > 0) {
+ mEntryListWrapper = new AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>
+ (mData, new NonPgpPart(nonPgpCounter));
+ }
+ }
+
+ private void addToData(PGPKeyRing keyring) {
+ ImportKeysListEntry item = new ImportKeysListEntry(keyring);
+ mData.add(item);
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListServerLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListServerLoader.java
new file mode 100644
index 000000000..259e14319
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListServerLoader.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.adapter;
+
+import android.content.Context;
+import android.support.v4.content.AsyncTaskLoader;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.util.HkpKeyServer;
+import org.sufficientlysecure.keychain.util.KeyServer;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.util.ArrayList;
+
+public class ImportKeysListServerLoader
+ extends AsyncTaskLoader<AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>> {
+ Context mContext;
+
+ String mServerQuery;
+ String mKeyServer;
+
+ private ArrayList<ImportKeysListEntry> mEntryList = new ArrayList<ImportKeysListEntry>();
+ private AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>> mEntryListWrapper;
+
+ public ImportKeysListServerLoader(Context context, String serverQuery, String keyServer) {
+ super(context);
+ mContext = context;
+ mServerQuery = serverQuery;
+ mKeyServer = keyServer;
+ }
+
+ @Override
+ public AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>> loadInBackground() {
+
+ mEntryListWrapper = new AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>(mEntryList, null);
+
+ if (mServerQuery == null) {
+ Log.e(Constants.TAG, "mServerQuery is null!");
+ return mEntryListWrapper;
+ }
+
+ if (mServerQuery.startsWith("0x") && mServerQuery.length() == 42) {
+ Log.d(Constants.TAG, "This search is based on a unique fingerprint. Enforce a fingerprint check!");
+ queryServer(mServerQuery, mKeyServer, true);
+ } else {
+ queryServer(mServerQuery, mKeyServer, false);
+ }
+
+ return mEntryListWrapper;
+ }
+
+ @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(AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>> data) {
+ super.deliverResult(data);
+ }
+
+ /**
+ * Query keyserver
+ */
+ private void queryServer(String query, String keyServer, boolean enforceFingerprint) {
+ HkpKeyServer server = new HkpKeyServer(keyServer);
+ try {
+ ArrayList<ImportKeysListEntry> searchResult = server.search(query);
+
+ mEntryList.clear();
+ // add result to data
+ if (enforceFingerprint) {
+ String fingerprint = query.substring(2);
+ Log.d(Constants.TAG, "fingerprint: " + fingerprint);
+ // query must return only one result!
+ if (searchResult.size() > 0) {
+ ImportKeysListEntry uniqueEntry = searchResult.get(0);
+ /*
+ * set fingerprint explicitly after query
+ * to enforce a check when the key is imported by KeychainIntentService
+ */
+ uniqueEntry.setFingerPrintHex(fingerprint);
+ uniqueEntry.setSelected(true);
+ mEntryList.add(uniqueEntry);
+ }
+ } else {
+ mEntryList.addAll(searchResult);
+ }
+ mEntryListWrapper = new AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>(mEntryList, null);
+ } catch (KeyServer.InsufficientQuery e) {
+ Log.e(Constants.TAG, "InsufficientQuery", e);
+ mEntryListWrapper = new AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>(mEntryList, e);
+ } catch (KeyServer.QueryException e) {
+ Log.e(Constants.TAG, "QueryException", e);
+ mEntryListWrapper = new AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>(mEntryList, e);
+ } catch (KeyServer.TooManyResponses e) {
+ Log.e(Constants.TAG, "TooManyResponses", e);
+ mEntryListWrapper = new AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>(mEntryList, e);
+ }
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyValueSpinnerAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyValueSpinnerAdapter.java
new file mode 100644
index 000000000..5b5d316b6
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyValueSpinnerAdapter.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.adapter;
+
+import android.content.Context;
+import android.widget.ArrayAdapter;
+
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+public class KeyValueSpinnerAdapter extends ArrayAdapter<String> {
+ private final HashMap<Integer, String> mData;
+ private final int[] mKeys;
+ private final String[] mValues;
+
+ static <K, V extends Comparable<? super V>> SortedSet<Map.Entry<K, V>> entriesSortedByValues(
+ Map<K, V> map) {
+ SortedSet<Map.Entry<K, V>> sortedEntries = new TreeSet<Map.Entry<K, V>>(
+ new Comparator<Map.Entry<K, V>>() {
+ @Override
+ public int compare(Map.Entry<K, V> e1, Map.Entry<K, V> e2) {
+ return e1.getValue().compareTo(e2.getValue());
+ }
+ });
+ sortedEntries.addAll(map.entrySet());
+ return sortedEntries;
+ }
+
+ public KeyValueSpinnerAdapter(Context context, HashMap<Integer, String> objects) {
+ // To make the drop down a simple text box
+ super(context, android.R.layout.simple_spinner_item);
+ mData = objects;
+
+ // To make the drop down view a radio button list
+ setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+
+ SortedSet<Map.Entry<Integer, String>> sorted = entriesSortedByValues(objects);
+
+ // Assign hash keys with a position so that we can present and retrieve them
+ int i = 0;
+ mKeys = new int[mData.size()];
+ mValues = new String[mData.size()];
+ for (Map.Entry<Integer, String> entry : sorted) {
+ mKeys[i] = entry.getKey();
+ mValues[i] = entry.getValue();
+ i++;
+ }
+ }
+
+ public int getCount() {
+ return mData.size();
+ }
+
+ /**
+ * Returns the value
+ */
+ @Override
+ public String getItem(int position) {
+ // return the value based on the position. This is displayed in the list.
+ return mValues[position];
+ }
+
+ /**
+ * Returns item key
+ */
+ public long getItemId(int position) {
+ // Return an id to represent the item.
+
+ return mKeys[position];
+ }
+
+ /**
+ * Find position from key
+ */
+ public int getPosition(long itemId) {
+ for (int i = 0; i < mKeys.length; i++) {
+ if ((int) itemId == mKeys[i]) {
+ return i;
+ }
+ }
+ return -1;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/PagerTabStripAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/PagerTabStripAdapter.java
new file mode 100644
index 000000000..fd864eb09
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/PagerTabStripAdapter.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.adapter;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentPagerAdapter;
+import android.support.v7.app.ActionBarActivity;
+
+import java.util.ArrayList;
+
+public class PagerTabStripAdapter extends FragmentPagerAdapter {
+ private final Context mContext;
+ private final ArrayList<TabInfo> mTabs = new ArrayList<TabInfo>();
+
+ static final class TabInfo {
+ public final Class<?> clss;
+ public final Bundle args;
+ public final String title;
+
+ TabInfo(Class<?> clss, Bundle args, String title) {
+ this.clss = clss;
+ this.args = args;
+ this.title = title;
+ }
+ }
+
+ public PagerTabStripAdapter(ActionBarActivity activity) {
+ super(activity.getSupportFragmentManager());
+ mContext = activity;
+ }
+
+ public void addTab(Class<?> clss, Bundle args, String title) {
+ TabInfo info = new TabInfo(clss, args, title);
+ mTabs.add(info);
+ 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);
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+ return mTabs.get(position).title;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SelectKeyCursorAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SelectKeyCursorAdapter.java
new file mode 100644
index 000000000..fbbb9caa4
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SelectKeyCursorAdapter.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.adapter;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.ListView;
+import android.widget.TextView;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
+
+
+public class SelectKeyCursorAdapter extends HighlightQueryCursorAdapter {
+
+ protected int mKeyType;
+
+ private LayoutInflater mInflater;
+ private ListView mListView;
+
+ private int mIndexUserId;
+ private int mIndexMasterKeyId;
+ private int mIndexProjectionValid;
+ private int mIndexProjectionAvailable;
+
+ public static final String PROJECTION_ROW_AVAILABLE = "available";
+ public static final String PROJECTION_ROW_VALID = "valid";
+
+ public SelectKeyCursorAdapter(Context context, Cursor c, int flags, ListView listView,
+ int keyType) {
+ super(context, c, flags);
+
+ mInflater = LayoutInflater.from(context);
+ mListView = listView;
+ mKeyType = keyType;
+ initIndex(c);
+ }
+
+ @Override
+ public Cursor swapCursor(Cursor newCursor) {
+ initIndex(newCursor);
+
+ return super.swapCursor(newCursor);
+ }
+
+ /**
+ * Get column indexes for performance reasons just once in constructor and swapCursor. For a
+ * performance comparison see http://stackoverflow.com/a/17999582
+ *
+ * @param cursor
+ */
+ private void initIndex(Cursor cursor) {
+ if (cursor != null) {
+ mIndexUserId = cursor.getColumnIndexOrThrow(UserIds.USER_ID);
+ mIndexMasterKeyId = cursor.getColumnIndexOrThrow(KeyRings.MASTER_KEY_ID);
+ mIndexProjectionValid = cursor.getColumnIndexOrThrow(PROJECTION_ROW_VALID);
+ mIndexProjectionAvailable = cursor.getColumnIndexOrThrow(PROJECTION_ROW_AVAILABLE);
+ }
+ }
+
+ public String getUserId(int position) {
+ mCursor.moveToPosition(position);
+ return mCursor.getString(mIndexUserId);
+ }
+
+ public long getMasterKeyId(int position) {
+ mCursor.moveToPosition(position);
+ return mCursor.getLong(mIndexMasterKeyId);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ boolean valid = cursor.getInt(mIndexProjectionValid) > 0;
+
+ TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId);
+ TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest);
+ TextView keyId = (TextView) view.findViewById(R.id.keyId);
+ TextView status = (TextView) view.findViewById(R.id.status);
+
+ String userId = cursor.getString(mIndexUserId);
+ String[] userIdSplit = PgpKeyHelper.splitUserId(userId);
+
+ if (userIdSplit[0] != null) {
+ mainUserId.setText(highlightSearchQuery(userIdSplit[0]));
+ } else {
+ mainUserId.setText(R.string.user_id_no_name);
+ }
+ if (userIdSplit[1] != null) {
+ mainUserIdRest.setText(highlightSearchQuery(userIdSplit[1]));
+ } else {
+ mainUserIdRest.setText("");
+ }
+
+ // TODO: needed to key id to no?
+ keyId.setText(R.string.no_key);
+ long masterKeyId = cursor.getLong(mIndexMasterKeyId);
+ keyId.setText(PgpKeyHelper.convertKeyIdToHexShort(masterKeyId));
+
+ // TODO: needed to set unknown_status?
+ status.setText(R.string.unknown_status);
+ if (valid) {
+ if (mKeyType == Id.type.public_key) {
+ status.setText(R.string.can_encrypt);
+ } else {
+ status.setText(R.string.can_sign);
+ }
+ } else {
+ if (cursor.getInt(mIndexProjectionAvailable) > 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.no_key);
+ }
+ }
+
+ 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);
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/TabsAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/TabsAdapter.java
new file mode 100644
index 000000000..9ddfa90be
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/TabsAdapter.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.adapter;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentStatePagerAdapter;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.view.ViewPager;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.ActionBarActivity;
+
+import java.util.ArrayList;
+
+public class TabsAdapter extends FragmentStatePagerAdapter 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 {
+ public final Class<?> clss;
+ public final Bundle args;
+
+ TabInfo(Class<?> clss, Bundle args) {
+ this.clss = clss;
+ this.args = args;
+ }
+ }
+
+ public TabsAdapter(ActionBarActivity 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(ActionBar.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(ActionBar.Tab tab, FragmentTransaction ft) {
+ }
+
+ public void onTabReselected(ActionBar.Tab tab, FragmentTransaction ft) {
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ViewKeyKeysAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ViewKeyKeysAdapter.java
new file mode 100644
index 000000000..64b735bfa
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ViewKeyKeysAdapter.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.adapter;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.database.Cursor;
+import android.support.v4.widget.CursorAdapter;
+import android.text.format.DateFormat;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.helper.OtherHelper;
+import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
+import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
+
+import java.util.Date;
+
+public class ViewKeyKeysAdapter extends CursorAdapter {
+ private LayoutInflater mInflater;
+
+ private int mIndexKeyId;
+ private int mIndexAlgorithm;
+ private int mIndexKeySize;
+ private int mIndexRank;
+ private int mIndexCanCertify;
+ private int mIndexCanEncrypt;
+ private int mIndexCanSign;
+ private int mIndexRevokedKey;
+ private int mIndexExpiry;
+
+ private ColorStateList mDefaultTextColor;
+
+ public ViewKeyKeysAdapter(Context context, Cursor c, int flags) {
+ super(context, c, flags);
+
+ mInflater = LayoutInflater.from(context);
+
+ initIndex(c);
+ }
+
+ @Override
+ public Cursor swapCursor(Cursor newCursor) {
+ initIndex(newCursor);
+
+ return super.swapCursor(newCursor);
+ }
+
+ /**
+ * Get column indexes for performance reasons just once in constructor and swapCursor. For a
+ * performance comparison see http://stackoverflow.com/a/17999582
+ *
+ * @param cursor
+ */
+ private void initIndex(Cursor cursor) {
+ if (cursor != null) {
+ mIndexKeyId = cursor.getColumnIndexOrThrow(Keys.KEY_ID);
+ mIndexAlgorithm = cursor.getColumnIndexOrThrow(Keys.ALGORITHM);
+ mIndexKeySize = cursor.getColumnIndexOrThrow(Keys.KEY_SIZE);
+ mIndexRank = cursor.getColumnIndexOrThrow(Keys.RANK);
+ mIndexCanCertify = cursor.getColumnIndexOrThrow(Keys.CAN_CERTIFY);
+ mIndexCanEncrypt = cursor.getColumnIndexOrThrow(Keys.CAN_ENCRYPT);
+ mIndexCanSign = cursor.getColumnIndexOrThrow(Keys.CAN_SIGN);
+ mIndexRevokedKey = cursor.getColumnIndexOrThrow(Keys.IS_REVOKED);
+ mIndexExpiry = cursor.getColumnIndexOrThrow(Keys.EXPIRY);
+ }
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ TextView keyId = (TextView) view.findViewById(R.id.keyId);
+ TextView keyDetails = (TextView) view.findViewById(R.id.keyDetails);
+ TextView keyExpiry = (TextView) view.findViewById(R.id.keyExpiry);
+ ImageView masterKeyIcon = (ImageView) view.findViewById(R.id.ic_masterKey);
+ ImageView certifyIcon = (ImageView) view.findViewById(R.id.ic_certifyKey);
+ ImageView encryptIcon = (ImageView) view.findViewById(R.id.ic_encryptKey);
+ ImageView signIcon = (ImageView) view.findViewById(R.id.ic_signKey);
+ ImageView revokedKeyIcon = (ImageView) view.findViewById(R.id.ic_revokedKey);
+
+ String keyIdStr = PgpKeyHelper.convertKeyIdToHexShort(cursor.getLong(mIndexKeyId));
+ String algorithmStr = PgpKeyHelper.getAlgorithmInfo(cursor.getInt(mIndexAlgorithm),
+ cursor.getInt(mIndexKeySize));
+
+ keyId.setText(keyIdStr);
+ keyDetails.setText("(" + algorithmStr + ")");
+
+ if (cursor.getInt(mIndexRank) == 0) {
+ masterKeyIcon.setVisibility(View.INVISIBLE);
+ } else {
+ masterKeyIcon.setVisibility(View.VISIBLE);
+ }
+
+ if (cursor.getInt(mIndexCanCertify) != 1) {
+ certifyIcon.setVisibility(View.GONE);
+ } else {
+ certifyIcon.setVisibility(View.VISIBLE);
+ }
+
+ if (cursor.getInt(mIndexCanEncrypt) != 1) {
+ encryptIcon.setVisibility(View.GONE);
+ } else {
+ encryptIcon.setVisibility(View.VISIBLE);
+ }
+
+ if (cursor.getInt(mIndexCanSign) != 1) {
+ signIcon.setVisibility(View.GONE);
+ } else {
+ signIcon.setVisibility(View.VISIBLE);
+ }
+
+ boolean valid = true;
+ if (cursor.getInt(mIndexRevokedKey) > 0) {
+ revokedKeyIcon.setVisibility(View.VISIBLE);
+
+ valid = false;
+ } else {
+ keyId.setTextColor(mDefaultTextColor);
+ keyDetails.setTextColor(mDefaultTextColor);
+ keyExpiry.setTextColor(mDefaultTextColor);
+
+ revokedKeyIcon.setVisibility(View.GONE);
+ }
+
+ if (!cursor.isNull(mIndexExpiry)) {
+ Date expiryDate = new Date(cursor.getLong(mIndexExpiry) * 1000);
+
+ valid = valid && expiryDate.after(new Date());
+ keyExpiry.setText("(" +
+ context.getString(R.string.label_expiry) + ": " +
+ DateFormat.getDateFormat(context).format(expiryDate) + ")");
+
+ keyExpiry.setVisibility(View.VISIBLE);
+ } else {
+ keyExpiry.setVisibility(View.GONE);
+ }
+ // if key is expired or revoked, strike through text
+ if (!valid) {
+ keyId.setText(OtherHelper.strikeOutText(keyId.getText()));
+ keyDetails.setText(OtherHelper.strikeOutText(keyDetails.getText()));
+ keyExpiry.setText(OtherHelper.strikeOutText(keyExpiry.getText()));
+ }
+ keyId.setEnabled(valid);
+ keyDetails.setEnabled(valid);
+ keyExpiry.setEnabled(valid);
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ View view = mInflater.inflate(R.layout.view_key_keys_item, null);
+ if (mDefaultTextColor == null) {
+ TextView keyId = (TextView) view.findViewById(R.id.keyId);
+ mDefaultTextColor = keyId.getTextColors();
+ }
+ return view;
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ViewKeyUserIdsAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ViewKeyUserIdsAdapter.java
new file mode 100644
index 000000000..09137f745
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ViewKeyUserIdsAdapter.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.adapter;
+
+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.AdapterView;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
+import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
+
+import java.util.ArrayList;
+
+public class ViewKeyUserIdsAdapter extends CursorAdapter implements AdapterView.OnItemClickListener {
+ private LayoutInflater mInflater;
+
+ private int mIndexUserId, mIndexRank;
+ private int mVerifiedId, mIsRevoked, mIsPrimary;
+
+ private final ArrayList<Boolean> mCheckStates;
+
+ public static final String[] USER_IDS_PROJECTION = new String[] {
+ UserIds._ID, UserIds.USER_ID, UserIds.RANK,
+ UserIds.VERIFIED, UserIds.IS_PRIMARY, UserIds.IS_REVOKED
+ };
+
+ public ViewKeyUserIdsAdapter(Context context, Cursor c, int flags, boolean showCheckBoxes) {
+ super(context, c, flags);
+
+ mInflater = LayoutInflater.from(context);
+
+ mCheckStates = showCheckBoxes ? new ArrayList<Boolean>() : null;
+
+ initIndex(c);
+ }
+ public ViewKeyUserIdsAdapter(Context context, Cursor c, int flags) {
+ this(context, c, flags, false);
+ }
+
+ @Override
+ public Cursor swapCursor(Cursor newCursor) {
+ initIndex(newCursor);
+ if (mCheckStates != null) {
+ mCheckStates.clear();
+ if (newCursor != null) {
+ int count = newCursor.getCount();
+ mCheckStates.ensureCapacity(count);
+ // initialize to true (use case knowledge: we usually want to sign all uids)
+ for(int i = 0; i < count; i++) {
+ newCursor.moveToPosition(i);
+ int verified = newCursor.getInt(mVerifiedId);
+ mCheckStates.add(verified != Certs.VERIFIED_SECRET);
+ }
+ }
+ }
+
+ return super.swapCursor(newCursor);
+ }
+
+ /**
+ * Get column indexes for performance reasons just once in constructor and swapCursor. For a
+ * performance comparison see http://stackoverflow.com/a/17999582
+ *
+ * @param cursor
+ */
+ private void initIndex(Cursor cursor) {
+ if (cursor != null) {
+ mIndexUserId = cursor.getColumnIndexOrThrow(UserIds.USER_ID);
+ mIndexRank = cursor.getColumnIndexOrThrow(UserIds.RANK);
+ mVerifiedId = cursor.getColumnIndexOrThrow(UserIds.VERIFIED);
+ mIsRevoked = cursor.getColumnIndexOrThrow(UserIds.IS_REVOKED);
+ mIsPrimary = cursor.getColumnIndexOrThrow(UserIds.IS_PRIMARY);
+ }
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+
+ TextView vRank = (TextView) view.findViewById(R.id.rank);
+ TextView vUserId = (TextView) view.findViewById(R.id.userId);
+ TextView vAddress = (TextView) view.findViewById(R.id.address);
+ ImageView vVerified = (ImageView) view.findViewById(R.id.certified);
+
+ if(cursor.getInt(mIsPrimary) > 0) {
+ vRank.setText("+");
+ } else {
+ vRank.setText(Integer.toString(cursor.getInt(mIndexRank)));
+ }
+
+ String[] userId = PgpKeyHelper.splitUserId(cursor.getString(mIndexUserId));
+ if (userId[0] != null) {
+ vUserId.setText(userId[0]);
+ } else {
+ vUserId.setText(R.string.user_id_no_name);
+ }
+ vAddress.setText(userId[1]);
+
+ if(cursor.getInt(mIsRevoked) > 0) {
+ vRank.setText(" ");
+ vVerified.setImageResource(android.R.drawable.presence_away);
+ } else {
+ int verified = cursor.getInt(mVerifiedId);
+ // TODO introduce own resources for this :)
+ if(verified == Certs.VERIFIED_SECRET)
+ vVerified.setImageResource(android.R.drawable.presence_online);
+ else if(verified == Certs.VERIFIED_SELF)
+ vVerified.setImageResource(android.R.drawable.presence_invisible);
+ else
+ vVerified.setImageResource(android.R.drawable.presence_busy);
+ }
+
+ // don't care further if checkboxes aren't shown
+ if (mCheckStates == null) {
+ return;
+ }
+
+ final CheckBox vCheckBox = (CheckBox) view.findViewById(R.id.checkBox);
+ final int position = cursor.getPosition();
+ vCheckBox.setOnCheckedChangeListener(null);
+ vCheckBox.setChecked(mCheckStates.get(position));
+ vCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
+ mCheckStates.set(position, b);
+ }
+ });
+ vCheckBox.setClickable(false);
+
+ }
+
+ public void onItemClick(AdapterView<?> adapter, View view, int position, long id) {
+ CheckBox box = ((CheckBox) view.findViewById(R.id.checkBox));
+ if(box != null) {
+ box.toggle();
+ }
+ }
+
+ public ArrayList<String> getSelectedUserIds() {
+ ArrayList<String> result = new ArrayList<String>();
+ for (int i = 0; i < mCheckStates.size(); i++) {
+ if (mCheckStates.get(i)) {
+ mCursor.moveToPosition(i);
+ result.add(mCursor.getString(mIndexUserId));
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ View view = mInflater.inflate(R.layout.view_key_userids_item, null);
+ // only need to do this once ever, since mShowCheckBoxes is final
+ view.findViewById(R.id.checkBox).setVisibility(mCheckStates != null ? View.VISIBLE : View.GONE);
+ return view;
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/BadImportKeyDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/BadImportKeyDialogFragment.java
new file mode 100644
index 000000000..20b70658c
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/BadImportKeyDialogFragment.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.dialog;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.FragmentActivity;
+import org.sufficientlysecure.keychain.R;
+
+public class BadImportKeyDialogFragment extends DialogFragment {
+ private static final String ARG_BAD_IMPORT = "bad_import";
+
+ /**
+ * Creates a new instance of this Bad Import Key DialogFragment
+ *
+ * @param bad
+ * @return
+ */
+ public static BadImportKeyDialogFragment newInstance(int bad) {
+ BadImportKeyDialogFragment frag = new BadImportKeyDialogFragment();
+ Bundle args = new Bundle();
+
+ args.putInt(ARG_BAD_IMPORT, bad);
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final FragmentActivity activity = getActivity();
+ final int badImport = getArguments().getInt(ARG_BAD_IMPORT);
+
+ AlertDialog.Builder alert = new AlertDialog.Builder(activity);
+ alert.setIcon(R.drawable.ic_dialog_alert_holo_light);
+ alert.setTitle(R.string.warning);
+ alert.setMessage(activity.getResources()
+ .getQuantityString(R.plurals.bad_keys_encountered, badImport, badImport));
+ alert.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ });
+ alert.setCancelable(true);
+
+ return alert.create();
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/CreateKeyDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/CreateKeyDialogFragment.java
new file mode 100644
index 000000000..ad558a81e
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/CreateKeyDialogFragment.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.dialog;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.FragmentActivity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Spinner;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.util.Choice;
+
+import java.util.ArrayList;
+
+public class CreateKeyDialogFragment extends DialogFragment {
+
+ public interface OnAlgorithmSelectedListener {
+ public void onAlgorithmSelected(Choice algorithmChoice, int keySize);
+ }
+
+ private static final String ARG_EDITOR_CHILD_COUNT = "child_count";
+
+ private int mNewKeySize;
+ private Choice mNewKeyAlgorithmChoice;
+ private OnAlgorithmSelectedListener mAlgorithmSelectedListener;
+
+ public void setOnAlgorithmSelectedListener(OnAlgorithmSelectedListener listener) {
+ mAlgorithmSelectedListener = listener;
+ }
+
+ public static CreateKeyDialogFragment newInstance(int mEditorChildCount) {
+ CreateKeyDialogFragment frag = new CreateKeyDialogFragment();
+ Bundle args = new Bundle();
+
+ args.putInt(ARG_EDITOR_CHILD_COUNT, mEditorChildCount);
+
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final FragmentActivity context = getActivity();
+ final LayoutInflater mInflater;
+
+ final int childCount = getArguments().getInt(ARG_EDITOR_CHILD_COUNT);
+ mInflater = context.getLayoutInflater();
+
+ AlertDialog.Builder dialog = new AlertDialog.Builder(context);
+
+ View view = mInflater.inflate(R.layout.create_key_dialog, null);
+ dialog.setView(view);
+ dialog.setTitle(R.string.title_create_key);
+
+ boolean wouldBeMasterKey = (childCount == 0);
+
+ final Spinner algorithm = (Spinner) view.findViewById(R.id.create_key_algorithm);
+ ArrayList<Choice> choices = new ArrayList<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>(context,
+ 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 Spinner keySize = (Spinner) view.findViewById(R.id.create_key_size);
+ ArrayAdapter<CharSequence> keySizeAdapter = ArrayAdapter.createFromResource(
+ context, R.array.key_size_spinner_values,
+ android.R.layout.simple_spinner_item);
+ keySizeAdapter
+ .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ keySize.setAdapter(keySizeAdapter);
+ keySize.setSelection(3); // Default to 4096 for the key length
+ dialog.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface di, int id) {
+ di.dismiss();
+ try {
+ final String selectedItem = (String) keySize.getSelectedItem();
+ mNewKeySize = Integer.parseInt(selectedItem);
+ } catch (NumberFormatException e) {
+ mNewKeySize = 0;
+ }
+
+ mNewKeyAlgorithmChoice = (Choice) algorithm.getSelectedItem();
+ mAlgorithmSelectedListener.onAlgorithmSelected(mNewKeyAlgorithmChoice, mNewKeySize);
+ }
+ });
+
+ dialog.setCancelable(true);
+ dialog.setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface di, int id) {
+ di.dismiss();
+ }
+ });
+
+ final AlertDialog alertDialog = dialog.create();
+
+ final AdapterView.OnItemSelectedListener weakRsaListener = new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ final Choice selectedAlgorithm = (Choice) algorithm.getSelectedItem();
+ final int selectedKeySize = Integer.parseInt((String) keySize.getSelectedItem());
+ final boolean isWeakRsa = (selectedAlgorithm.getId() == Id.choice.algorithm.rsa &&
+ selectedKeySize <= 1024);
+ alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(!isWeakRsa);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ }
+ };
+
+ keySize.setOnItemSelectedListener(weakRsaListener);
+ algorithm.setOnItemSelectedListener(weakRsaListener);
+
+ return alertDialog;
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/DeleteFileDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/DeleteFileDialogFragment.java
new file mode 100644
index 000000000..b4c38184c
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/DeleteFileDialogFragment.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.dialog;
+
+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;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+
+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(R.drawable.ic_dialog_alert_holo_light);
+ alert.setTitle(R.string.warning);
+ alert.setMessage(this.getString(R.string.file_delete_confirmation, 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.setAction(KeychainIntentService.ACTION_DELETE_FILE_SECURELY);
+ data.putString(KeychainIntentService.DELETE_FILE, deleteFile);
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ ProgressDialogFragment deletingDialog = ProgressDialogFragment.newInstance(
+ getString(R.string.progress_deleting_securely),
+ ProgressDialog.STYLE_HORIZONTAL,
+ false,
+ null);
+
+ // Message is received after deleting is done in KeychainIntentService
+ KeychainIntentServiceHandler saveHandler =
+ new KeychainIntentServiceHandler(activity, deletingDialog) {
+ public void handleMessage(Message message) {
+ // handle messages by standard KeychainIntentHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ Toast.makeText(activity, R.string.file_delete_successful,
+ 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();
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java
new file mode 100644
index 000000000..72ea4c013
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.dialog;
+
+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;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.provider.KeychainDatabase;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.util.HashMap;
+
+public class DeleteKeyDialogFragment extends DialogFragment {
+ private static final String ARG_MESSENGER = "messenger";
+ private static final String ARG_DELETE_MASTER_KEY_IDS = "delete_master_key_ids";
+
+ public static final int MESSAGE_OKAY = 1;
+ public static final int MESSAGE_ERROR = 0;
+
+ private boolean mIsSingleSelection = false;
+
+ private TextView mMainMessage;
+ private CheckBox mCheckDeleteSecret;
+ private LinearLayout mDeleteSecretKeyView;
+ private View mInflateView;
+
+ private Messenger mMessenger;
+
+ /**
+ * Creates new instance of this delete file dialog fragment
+ */
+ public static DeleteKeyDialogFragment newInstance(Messenger messenger,
+ long[] masterKeyIds) {
+ DeleteKeyDialogFragment frag = new DeleteKeyDialogFragment();
+ Bundle args = new Bundle();
+
+ args.putParcelable(ARG_MESSENGER, messenger);
+ args.putLongArray(ARG_DELETE_MASTER_KEY_IDS, masterKeyIds);
+ //We don't need the key type
+
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+
+ final FragmentActivity activity = getActivity();
+ mMessenger = getArguments().getParcelable(ARG_MESSENGER);
+
+ final long[] masterKeyIds = getArguments().getLongArray(ARG_DELETE_MASTER_KEY_IDS);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+
+ //Setup custom View to display in AlertDialog
+ LayoutInflater inflater = activity.getLayoutInflater();
+ mInflateView = inflater.inflate(R.layout.view_key_delete_fragment, null);
+ builder.setView(mInflateView);
+
+ mDeleteSecretKeyView = (LinearLayout) mInflateView.findViewById(R.id.deleteSecretKeyView);
+ mMainMessage = (TextView) mInflateView.findViewById(R.id.mainMessage);
+ mCheckDeleteSecret = (CheckBox) mInflateView.findViewById(R.id.checkDeleteSecret);
+
+ builder.setTitle(R.string.warning);
+
+ // If only a single key has been selected
+ if (masterKeyIds.length == 1) {
+ mIsSingleSelection = true;
+
+ long masterKeyId = masterKeyIds[0];
+
+ HashMap<String, Object> data = ProviderHelper.getUnifiedData(activity, masterKeyId, new String[]{
+ KeyRings.USER_ID,
+ KeyRings.HAS_SECRET
+ }, new int[] { ProviderHelper.FIELD_TYPE_STRING, ProviderHelper.FIELD_TYPE_INTEGER });
+ String userId = (String) data.get(KeyRings.USER_ID);
+ boolean hasSecret = ((Long) data.get(KeyRings.HAS_SECRET)) == 1;
+
+ // Hide the Checkbox and TextView since this is a single selection,user will be notified through message
+ mDeleteSecretKeyView.setVisibility(View.GONE);
+ // Set message depending on which key it is.
+ mMainMessage.setText(getString(
+ hasSecret ? R.string.secret_key_deletion_confirmation
+ : R.string.public_key_deletetion_confirmation,
+ userId));
+ } else {
+ mDeleteSecretKeyView.setVisibility(View.VISIBLE);
+ mMainMessage.setText(R.string.key_deletion_confirmation_multi);
+ }
+
+ builder.setIcon(R.drawable.ic_dialog_alert_holo_light);
+ builder.setPositiveButton(R.string.btn_delete, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+
+ boolean success = false;
+ for(long masterKeyId : masterKeyIds) {
+ int count = activity.getContentResolver().delete(
+ KeyRingData.buildPublicKeyRingUri(Long.toString(masterKeyId)), null, null
+ );
+ success = count > 0;
+ }
+ if (success) {
+ sendMessageToHandler(MESSAGE_OKAY, null);
+ } else {
+ sendMessageToHandler(MESSAGE_ERROR, null);
+ }
+ dismiss();
+ }
+ });
+ 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, 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);
+ }
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java
new file mode 100644
index 000000000..a4285c8e9
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.dialog;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Message;
+import android.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.TextView;
+import com.beardedhen.androidbootstrap.BootstrapButton;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.helper.FileHelper;
+import org.sufficientlysecure.keychain.util.Log;
+
+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";
+
+ 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;
+
+ private EditText mFilename;
+ private BootstrapButton mBrowse;
+ private CheckBox mCheckBox;
+ private TextView mMessageTextView;
+
+ private static final int REQUEST_CODE = 0x00007004;
+
+ /**
+ * Creates new instance of this file dialog fragment
+ */
+ public static FileDialogFragment newInstance(Messenger messenger, String title, String message,
+ String defaultFile, String checkboxText) {
+ 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);
+
+ 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);
+
+ LayoutInflater inflater = (LayoutInflater) activity
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ AlertDialog.Builder alert = new AlertDialog.Builder(activity);
+ alert.setTitle(title);
+
+ View view = inflater.inflate(R.layout.file_dialog, null);
+
+ mMessageTextView = (TextView) view.findViewById(R.id.message);
+ mMessageTextView.setText(message);
+
+ mFilename = (EditText) view.findViewById(R.id.input);
+ mFilename.setText(defaultFile);
+ mBrowse = (BootstrapButton) view.findViewById(R.id.btn_browse);
+ mBrowse.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ // only .asc or .gpg files
+ // setting it to text/plain prevents Cynaogenmod's file manager from selecting asc
+ // or gpg types!
+ FileHelper.openFile(FileDialogFragment.this, mFilename.getText().toString(), "*/*",
+ REQUEST_CODE);
+ }
+ });
+
+ 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
+ */
+ private void setFilename(String filename) {
+ AlertDialog dialog = (AlertDialog) getDialog();
+ EditText filenameEditText = (EditText) dialog.findViewById(R.id.input);
+
+ if (filenameEditText != null) {
+ filenameEditText.setText(filename);
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode & 0xFFFF) {
+ case REQUEST_CODE: {
+ if (resultCode == Activity.RESULT_OK && data != null) {
+ try {
+ String path = data.getData().getPath();
+ Log.d(Constants.TAG, "path=" + path);
+
+ // set filename used in export/import dialogs
+ setFilename(path);
+ } catch (NullPointerException e) {
+ Log.e(Constants.TAG, "Nullpointer while retrieving path!", e);
+ }
+ }
+
+ break;
+ }
+
+ default:
+ super.onActivityResult(requestCode, resultCode, data);
+
+ break;
+ }
+ }
+
+ /**
+ * 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);
+ }
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java
new file mode 100644
index 000000000..a3feab959
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.dialog;
+
+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.Handler;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.FragmentActivity;
+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;
+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.R;
+import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
+import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.service.PassphraseCacheService;
+import org.sufficientlysecure.keychain.util.Log;
+
+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;
+ public static final int MESSAGE_CANCEL = 2;
+
+ public static final String MESSAGE_DATA_PASSPHRASE = "passphrase";
+
+ private Messenger mMessenger;
+ private EditText mPassphraseEditText;
+ private boolean mCanKB;
+
+ /**
+ * 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
+ */
+ public static void show(FragmentActivity context, long keyId, Handler returnHandler) {
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(returnHandler);
+
+ try {
+ PassphraseDialogFragment passphraseDialog = PassphraseDialogFragment.newInstance(context,
+ messenger, keyId);
+
+ passphraseDialog.show(context.getSupportFragmentManager(), "passphraseDialog");
+ } catch (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);
+ }
+ }
+
+ /**
+ * 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 (!PassphraseCacheService.hasPassphrase(context, secretKeyId)) {
+ throw new 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;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ /**
+ * Creates dialog
+ */
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Activity activity = getActivity();
+ final 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.passphrase_for_symmetric_encryption);
+ } else {
+ secretKey = ProviderHelper.getPGPSecretKeyRing(activity, secretKeyId).getSecretKey();
+
+ if (secretKey == null) {
+ alert.setTitle(R.string.title_key_not_found);
+ alert.setMessage(getString(R.string.key_not_found, secretKeyId));
+ alert.setPositiveButton(android.R.string.ok, new OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ dismiss();
+ }
+ });
+ alert.setCancelable(false);
+ mCanKB = false;
+ return alert.create();
+ }
+ String userId = PgpKeyHelper.getMainUserIdSafe(activity, secretKey);
+
+ Log.d(Constants.TAG, "User id: '" + userId + "'");
+ alert.setMessage(getString(R.string.passphrase_for, userId));
+ }
+
+ LayoutInflater inflater = activity.getLayoutInflater();
+ View view = inflater.inflate(R.layout.passphrase_dialog, 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();
+ long curKeyIndex = 1;
+ boolean keyOK = true;
+ String passphrase = mPassphraseEditText.getText().toString();
+ long keyId;
+ PGPSecretKey clickSecretKey = secretKey;
+
+ if (clickSecretKey != null) {
+ while (keyOK) {
+ if (clickSecretKey != null) { // check again for loop
+ try {
+ PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder()
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(
+ passphrase.toCharArray());
+ PGPPrivateKey testKey = clickSecretKey
+ .extractPrivateKey(keyDecryptor);
+ if (testKey == null) {
+ if (!clickSecretKey.isMasterKey()) {
+ Toast.makeText(activity,
+ R.string.error_could_not_extract_private_key,
+ Toast.LENGTH_SHORT).show();
+
+ sendMessageToHandler(MESSAGE_CANCEL);
+ return;
+ } else {
+ clickSecretKey = PgpKeyHelper.getKeyNum(ProviderHelper
+ .getPGPSecretKeyRingWithKeyId(activity, secretKeyId),
+ curKeyIndex);
+ curKeyIndex++; // does post-increment work like C?
+ continue;
+ }
+ } else {
+ keyOK = false;
+ }
+ } catch (PGPException e) {
+ Toast.makeText(activity, R.string.wrong_passphrase,
+ Toast.LENGTH_SHORT).show();
+
+ sendMessageToHandler(MESSAGE_CANCEL);
+ return;
+ }
+ } else {
+ Toast.makeText(activity, R.string.error_could_not_extract_private_key,
+ Toast.LENGTH_SHORT).show();
+
+ sendMessageToHandler(MESSAGE_CANCEL);
+ return; // ran out of keys to try
+ }
+ }
+ 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);
+ if (!keyOK && clickSecretKey.getKeyID() != keyId) {
+ PassphraseCacheService.addCachedPassphrase(activity, clickSecretKey.getKeyID(),
+ passphrase);
+ }
+
+ // also return passphrase back to activity
+ Bundle data = new Bundle();
+ data.putString(MESSAGE_DATA_PASSPHRASE, passphrase);
+
+ sendMessageToHandler(MESSAGE_OKAY, data);
+ }
+ });
+
+ alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ });
+
+ mCanKB = true;
+ return alert.create();
+ }
+
+ @Override
+ public void onActivityCreated(Bundle arg0) {
+ super.onActivityCreated(arg0);
+ if (mCanKB) {
+ // request focus and open soft keyboard
+ mPassphraseEditText.requestFocus();
+ getDialog().getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_VISIBLE);
+
+ mPassphraseEditText.setOnEditorActionListener(this);
+ }
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ super.onCancel(dialog);
+
+ dismiss();
+ sendMessageToHandler(MESSAGE_CANCEL);
+ }
+
+ /**
+ * 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);
+ }
+ }
+
+ /**
+ * 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);
+ }
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ProgressDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ProgressDialogFragment.java
new file mode 100644
index 000000000..132a2ce86
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ProgressDialogFragment.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+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.OnCancelListener;
+import android.content.DialogInterface.OnKeyListener;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.view.KeyEvent;
+import org.sufficientlysecure.keychain.R;
+
+public class ProgressDialogFragment extends DialogFragment {
+ private static final String ARG_MESSAGE = "message";
+ private static final String ARG_STYLE = "style";
+ private static final String ARG_CANCELABLE = "cancelable";
+
+ private OnCancelListener mOnCancelListener;
+
+ /**
+ * Creates new instance of this fragment
+ *
+ * @param message
+ * @param style
+ * @param cancelable
+ * @return
+ */
+ public static ProgressDialogFragment newInstance(String message, int style, boolean cancelable,
+ OnCancelListener onCancelListener) {
+ ProgressDialogFragment frag = new ProgressDialogFragment();
+ Bundle args = new Bundle();
+ args.putString(ARG_MESSAGE, message);
+ args.putInt(ARG_STYLE, style);
+ args.putBoolean(ARG_CANCELABLE, cancelable);
+
+ frag.setArguments(args);
+ frag.mOnCancelListener = onCancelListener;
+
+ 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 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 message
+ * @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);
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ super.onCancel(dialog);
+
+ if (this.mOnCancelListener != null) {
+ this.mOnCancelListener.onCancel(dialog);
+ }
+ }
+
+ /**
+ * Creates dialog
+ */
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Activity activity = getActivity();
+
+ ProgressDialog dialog = new ProgressDialog(activity);
+ dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ dialog.setCancelable(false);
+ dialog.setCanceledOnTouchOutside(false);
+
+ String message = getArguments().getString(ARG_MESSAGE);
+ int style = getArguments().getInt(ARG_STYLE);
+ boolean cancelable = getArguments().getBoolean(ARG_CANCELABLE);
+
+ dialog.setMessage(message);
+ dialog.setProgressStyle(style);
+
+ if (cancelable) {
+ dialog.setButton(DialogInterface.BUTTON_NEGATIVE,
+ activity.getString(R.string.progress_cancel),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.cancel();
+ }
+ });
+ }
+
+ // 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;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java
new file mode 100644
index 000000000..ae61c1470
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.dialog;
+
+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.TextView.OnEditorActionListener;
+import android.widget.Toast;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.util.Log;
+
+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.enter_passphrase_twice);
+
+ LayoutInflater inflater = activity.getLayoutInflater();
+ View view = inflater.inflate(R.layout.passphrase_repeat_dialog, 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.error_message,
+ getString(R.string.passphrases_do_not_match)), Toast.LENGTH_SHORT)
+ .show();
+ return;
+ }
+
+ if (passphrase1.equals("")) {
+ Toast.makeText(
+ activity,
+ getString(R.string.error_message,
+ getString(R.string.passphrase_must_not_be_empty)),
+ 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);
+ }
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ShareNfcDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ShareNfcDialogFragment.java
new file mode 100644
index 000000000..741530b1d
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ShareNfcDialogFragment.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.dialog;
+
+import android.annotation.TargetApi;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.nfc.NfcAdapter;
+import android.os.Build;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.FragmentActivity;
+import org.sufficientlysecure.htmltextview.HtmlTextView;
+import org.sufficientlysecure.keychain.R;
+
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+public class ShareNfcDialogFragment extends DialogFragment {
+
+ /**
+ * Creates new instance of this fragment
+ */
+ public static ShareNfcDialogFragment newInstance() {
+ ShareNfcDialogFragment frag = new ShareNfcDialogFragment();
+
+ return frag;
+ }
+
+ /**
+ * Creates dialog
+ */
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final FragmentActivity activity = getActivity();
+
+ AlertDialog.Builder alert = new AlertDialog.Builder(activity);
+
+ alert.setTitle(R.string.share_nfc_dialog);
+ alert.setCancelable(true);
+
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+ }
+ });
+
+ HtmlTextView textView = new HtmlTextView(getActivity());
+ textView.setPadding(8, 8, 8, 8);
+ alert.setView(textView);
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
+ textView.setText(getString(R.string.error) + ": "
+ + getString(R.string.error_jelly_bean_needed));
+ } else {
+ // check if NFC Adapter is available
+ NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(getActivity());
+ if (nfcAdapter == null) {
+ textView.setText(getString(R.string.error) + ": "
+ + getString(R.string.error_nfc_needed));
+ } else {
+ // nfc works...
+ textView.setHtmlFromRawResource(getActivity(), R.raw.nfc_beam_share);
+
+ alert.setNegativeButton(R.string.menu_beam_preferences,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ Intent intentSettings = new Intent(
+ Settings.ACTION_NFCSHARING_SETTINGS);
+ startActivity(intentSettings);
+ }
+ });
+ }
+ }
+
+ // no flickering when clicking textview for Android < 4
+ // aboutTextView.setTextColor(getResources().getColor(android.R.color.black));
+
+ return alert.create();
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ShareQrCodeDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ShareQrCodeDialogFragment.java
new file mode 100644
index 000000000..b6ff139df
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ShareQrCodeDialogFragment.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.dialog;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.util.QrCodeUtils;
+
+import java.util.ArrayList;
+
+public class ShareQrCodeDialogFragment extends DialogFragment {
+ private static final String ARG_KEY_URI = "uri";
+ private static final String ARG_FINGERPRINT_ONLY = "fingerprint_only";
+
+ private ImageView mImage;
+ private TextView mText;
+
+ private boolean mFingerprintOnly;
+
+ private ArrayList<String> mContentList;
+ private int mCounter;
+
+ private static final int QR_CODE_SIZE = 1000;
+
+ /**
+ * Creates new instance of this dialog fragment
+ */
+ public static ShareQrCodeDialogFragment newInstance(Uri dataUri, boolean fingerprintOnly) {
+ ShareQrCodeDialogFragment frag = new ShareQrCodeDialogFragment();
+ Bundle args = new Bundle();
+ args.putParcelable(ARG_KEY_URI, dataUri);
+ args.putBoolean(ARG_FINGERPRINT_ONLY, fingerprintOnly);
+
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ /**
+ * Creates dialog
+ */
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Activity activity = getActivity();
+
+ Uri dataUri = getArguments().getParcelable(ARG_KEY_URI);
+ mFingerprintOnly = getArguments().getBoolean(ARG_FINGERPRINT_ONLY);
+
+ AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
+
+ alert.setTitle(R.string.share_qr_code_dialog_title);
+
+ LayoutInflater inflater = activity.getLayoutInflater();
+ View view = inflater.inflate(R.layout.share_qr_code_dialog, null);
+ alert.setView(view);
+
+ mImage = (ImageView) view.findViewById(R.id.share_qr_code_dialog_image);
+ mText = (TextView) view.findViewById(R.id.share_qr_code_dialog_text);
+
+ String content = null;
+ if (mFingerprintOnly) {
+ alert.setPositiveButton(R.string.btn_okay, null);
+
+ byte[] blob = (byte[]) ProviderHelper.getGenericData(
+ getActivity(), KeyRings.buildUnifiedKeyRingUri(dataUri),
+ KeyRings.FINGERPRINT, ProviderHelper.FIELD_TYPE_BLOB);
+ if(blob == null) {
+ // TODO error handling?!
+ return null;
+ }
+
+ String fingerprint = PgpKeyHelper.convertFingerprintToHex(blob);
+ mText.setText(getString(R.string.share_qr_code_dialog_fingerprint_text) + " " + fingerprint);
+ content = Constants.FINGERPRINT_SCHEME + ":" + fingerprint;
+ setQrCode(content);
+ } else {
+ mText.setText(R.string.share_qr_code_dialog_start);
+
+ // TODO works, but
+ long masterKeyId = ProviderHelper.getMasterKeyId(getActivity(), dataUri);
+ // get public keyring as ascii armored string
+ ArrayList<String> keyringArmored = ProviderHelper.getKeyRingsAsArmoredString(
+ getActivity(), new long[] { masterKeyId });
+
+ // TODO: binary?
+
+ content = keyringArmored.get(0);
+
+ // OnClickListener are set in onResume to prevent automatic dismissing of Dialogs
+ // http://bit.ly/O5vfaR
+ alert.setPositiveButton(R.string.btn_next, null);
+ alert.setNegativeButton(android.R.string.cancel, null);
+
+ mContentList = splitString(content, 1000);
+
+ // start with first
+ mCounter = 0;
+ updatePartsQrCode();
+ }
+
+ return alert.create();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ if (!mFingerprintOnly) {
+ AlertDialog alertDialog = (AlertDialog) getDialog();
+ final Button backButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE);
+ final Button nextButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
+
+ backButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mCounter > 0) {
+ mCounter--;
+ updatePartsQrCode();
+ updateDialog(backButton, nextButton);
+ } else {
+ dismiss();
+ }
+ }
+ });
+ nextButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+
+ if (mCounter < mContentList.size() - 1) {
+ mCounter++;
+ updatePartsQrCode();
+ updateDialog(backButton, nextButton);
+ } else {
+ dismiss();
+ }
+ }
+ });
+ }
+ }
+
+ private void updatePartsQrCode() {
+ // Content: <counter>,<size>,<content>
+ setQrCode(mCounter + "," + mContentList.size() + "," + mContentList.get(mCounter));
+ }
+
+ private void setQrCode(String data) {
+ mImage.setImageBitmap(QrCodeUtils.getQRCodeBitmap(data, QR_CODE_SIZE));
+ }
+
+ private void updateDialog(Button backButton, Button nextButton) {
+ if (mCounter == 0) {
+ backButton.setText(android.R.string.cancel);
+ } else {
+ backButton.setText(R.string.btn_back);
+ }
+ if (mCounter == mContentList.size() - 1) {
+ nextButton.setText(android.R.string.ok);
+ } else {
+ nextButton.setText(R.string.btn_next);
+ }
+
+ mText.setText(getResources().getString(R.string.share_qr_code_dialog_progress,
+ mCounter + 1, mContentList.size()));
+ }
+
+ /**
+ * Split String by number of characters
+ *
+ * @param text
+ * @param size
+ * @return
+ */
+ private ArrayList<String> splitString(String text, int size) {
+ ArrayList<String> strings = new ArrayList<String>();
+ int index = 0;
+ while (index < text.length()) {
+ strings.add(text.substring(index, Math.min(index + size, text.length())));
+ index += size;
+ }
+
+ return strings;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/Editor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/Editor.java
new file mode 100644
index 000000000..7b21c189d
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/Editor.java
@@ -0,0 +1,27 @@
+/*
+ * 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, boolean wasNewItem);
+ public void onEdited();
+ }
+
+ public void setEditorListener(EditorListener listener);
+ public boolean needsSaving();
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/FixedListView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/FixedListView.java
new file mode 100644
index 000000000..da29f808a
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/FixedListView.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ListView;
+
+/**
+ * Automatically calculate height of ListView based on contained items. This enables to put this
+ * ListView into a ScrollView without messing up.
+ * <p/>
+ * from
+ * http://stackoverflow.com/questions/2419246/how-do-i-create-a-listview-thats-not-in-a-scrollview-
+ * or-has-the-scrollview-dis
+ */
+public class FixedListView extends ListView {
+
+ public FixedListView(Context context) {
+ super(context);
+ }
+
+ public FixedListView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public FixedListView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // Calculate height of the entire list by providing a very large
+ // height hint. But do not use the highest 2 bits of this integer;
+ // those are reserved for the MeasureSpec mode.
+ int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
+ super.onMeasure(widthMeasureSpec, expandSpec);
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/FoldableLinearLayout.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/FoldableLinearLayout.java
new file mode 100644
index 000000000..6b2f3bf06
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/FoldableLinearLayout.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import com.beardedhen.androidbootstrap.FontAwesomeText;
+import org.sufficientlysecure.keychain.R;
+
+/**
+ * Class representing a LinearLayout that can fold and hide it's content when pressed
+ * To use just add the following to your xml layout
+
+ <org.sufficientlysecure.keychain.ui.widget.FoldableLinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ custom:foldedLabel="@string/TEXT_TO_DISPLAY_WHEN_FOLDED"
+ custom:unFoldedLabel="@string/TEXT_TO_DISPLAY_WHEN_UNFOLDED"
+ custom:foldedIcon="ICON_NAME_FROM_FontAwesomeText_TO_USE_WHEN_FOLDED"
+ custom:unFoldedIcon="ICON_NAME_FROM_FontAwesomeText_TO_USE_WHEN_UNFOLDED">
+
+ <include layout="@layout/ELEMENTS_TO_BE_FOLDED"/>
+
+ </org.sufficientlysecure.keychain.ui.widget.FoldableLinearLayout>
+
+ */
+public class FoldableLinearLayout extends LinearLayout {
+
+ private FontAwesomeText mFoldableIcon;
+ private boolean mFolded;
+ private boolean mHasMigrated = false;
+ private Integer mShortAnimationDuration = null;
+ private TextView mFoldableTextView = null;
+ private LinearLayout mFoldableContainer = null;
+ private View mFoldableLayout = null;
+
+ private String mFoldedIconName;
+ private String mUnFoldedIconName;
+ private String mFoldedLabel;
+ private String mUnFoldedLabel;
+
+ public FoldableLinearLayout(Context context) {
+ super(context);
+ processAttributes(context, null);
+ }
+
+ public FoldableLinearLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ processAttributes(context, attrs);
+ }
+
+ public FoldableLinearLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs);
+ processAttributes(context, attrs);
+ }
+
+ /**
+ * Load given attributes to inner variables,
+ * @param context
+ * @param attrs
+ */
+ private void processAttributes(Context context, AttributeSet attrs) {
+ if (attrs != null) {
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ R.styleable.FoldableLinearLayout, 0, 0);
+ mFoldedIconName = a.getString(R.styleable.FoldableLinearLayout_foldedIcon);
+ mUnFoldedIconName = a.getString(R.styleable.FoldableLinearLayout_unFoldedIcon);
+ mFoldedLabel = a.getString(R.styleable.FoldableLinearLayout_foldedLabel);
+ mUnFoldedLabel = a.getString(R.styleable.FoldableLinearLayout_unFoldedLabel);
+ a.recycle();
+ }
+ // If any attribute isn't found then set a default one
+ mFoldedIconName = (mFoldedIconName == null) ? "fa-chevron-right" : mFoldedIconName;
+ mUnFoldedIconName = (mUnFoldedIconName == null) ? "fa-chevron-down" : mUnFoldedIconName;
+ mFoldedLabel = (mFoldedLabel == null) ? context.getString(R.id.none) : mFoldedLabel;
+ mUnFoldedLabel = (mUnFoldedLabel == null) ? context.getString(R.id.none) : mUnFoldedLabel;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ // if the migration has already happened
+ // there is no need to move any children
+ if (!mHasMigrated) {
+ migrateChildrenToContainer();
+ mHasMigrated = true;
+ }
+
+ initialiseInnerViews();
+
+ super.onFinishInflate();
+ }
+
+ /**
+ * Migrates Child views as declared in xml to the inner foldableContainer
+ */
+ private void migrateChildrenToContainer() {
+ // Collect children of FoldableLinearLayout as declared in XML
+ int childNum = getChildCount();
+ View[] children = new View[childNum];
+
+ for (int i = 0; i < childNum; i++) {
+ children[i] = getChildAt(i);
+ }
+ if (children[0].getId() == R.id.foldableControl) {
+
+ }
+
+ // remove all of them from FoldableLinearLayout
+ detachAllViewsFromParent();
+
+ // Inflate the inner foldable_linearlayout.xml
+ LayoutInflater inflator = (LayoutInflater) getContext().getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+
+ mFoldableLayout = inflator.inflate(R.layout.foldable_linearlayout, this, true);
+ mFoldableContainer = (LinearLayout) mFoldableLayout.findViewById(R.id.foldableContainer);
+
+ // Push previously collected children into foldableContainer.
+ for (int i = 0; i < childNum; i++) {
+ addView(children[i]);
+ }
+ }
+
+ private void initialiseInnerViews() {
+ mFoldableIcon = (FontAwesomeText) mFoldableLayout.findViewById(R.id.foldableIcon);
+ mFoldableIcon.setIcon(mFoldedIconName);
+ mFoldableTextView = (TextView) mFoldableLayout.findViewById(R.id.foldableText);
+ mFoldableTextView.setText(mFoldedLabel);
+
+ // retrieve and cache the system's short animation time
+ mShortAnimationDuration = getResources().getInteger(android.R.integer.config_shortAnimTime);
+
+ LinearLayout foldableControl = (LinearLayout) mFoldableLayout.findViewById(R.id.foldableControl);
+ foldableControl.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ mFolded = !mFolded;
+ if (mFolded) {
+ mFoldableIcon.setIcon(mUnFoldedIconName);
+ mFoldableContainer.setVisibility(View.VISIBLE);
+ AlphaAnimation animation = new AlphaAnimation(0f, 1f);
+ animation.setDuration(mShortAnimationDuration);
+ mFoldableContainer.startAnimation(animation);
+ mFoldableTextView.setText(mUnFoldedLabel);
+
+ } else {
+ mFoldableIcon.setIcon(mFoldedIconName);
+ AlphaAnimation animation = new AlphaAnimation(1f, 0f);
+ animation.setDuration(mShortAnimationDuration);
+ animation.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) { }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ // making sure that at the end the container is completely removed from view
+ mFoldableContainer.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) { }
+ });
+ mFoldableContainer.startAnimation(animation);
+ mFoldableTextView.setText(mFoldedLabel);
+ }
+ }
+ });
+
+ }
+
+ /**
+ * Adds provided child view to foldableContainer View
+ * @param child
+ */
+ @Override
+ public void addView(View child) {
+ if (mFoldableContainer != null) {
+ mFoldableContainer.addView(child);
+ }
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/IntegerListPreference.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/IntegerListPreference.java
new file mode 100644
index 000000000..6e1e4c678
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/IntegerListPreference.java
@@ -0,0 +1,94 @@
+/*
+ * 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/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyEditor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyEditor.java
new file mode 100644
index 000000000..c7bd1c987
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyEditor.java
@@ -0,0 +1,377 @@
+/*
+ * 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 android.annotation.TargetApi;
+import android.app.DatePickerDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.text.format.DateUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.DatePicker;
+import android.widget.LinearLayout;
+import android.widget.TableLayout;
+import android.widget.TableRow;
+import android.widget.TextView;
+
+import com.beardedhen.androidbootstrap.BootstrapButton;
+
+import org.spongycastle.bcpg.sig.KeyFlags;
+import org.spongycastle.openpgp.PGPPublicKey;
+import org.spongycastle.openpgp.PGPSecretKey;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
+import org.sufficientlysecure.keychain.util.Choice;
+
+import java.text.DateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+import java.util.Vector;
+
+public class KeyEditor extends LinearLayout implements Editor, OnClickListener {
+ private PGPSecretKey mKey;
+
+ private EditorListener mEditorListener = null;
+
+ private boolean mIsMasterKey;
+ BootstrapButton mDeleteButton;
+ TextView mAlgorithm;
+ TextView mKeyId;
+ TextView mCreationDate;
+ BootstrapButton mExpiryDateButton;
+ GregorianCalendar mCreatedDate;
+ GregorianCalendar mExpiryDate;
+ GregorianCalendar mOriginalExpiryDate = null;
+ CheckBox mChkCertify;
+ CheckBox mChkSign;
+ CheckBox mChkEncrypt;
+ CheckBox mChkAuthenticate;
+ int mUsage;
+ int mOriginalUsage;
+ boolean mIsNewKey;
+
+ private CheckBox.OnCheckedChangeListener mCheckChanged = new CheckBox.OnCheckedChangeListener()
+ {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked)
+ {
+ if (mEditorListener != null) {
+ mEditorListener.onEdited();
+ }
+ }
+ };
+
+
+ private int mDatePickerResultCount = 0;
+ private DatePickerDialog.OnDateSetListener mExpiryDateSetListener =
+ new DatePickerDialog.OnDateSetListener() {
+ public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
+ // Note: Ignore results after the first one - android sends multiples.
+ if (mDatePickerResultCount++ == 0) {
+ GregorianCalendar date = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
+ date.set(year, monthOfYear, dayOfMonth);
+ if (mOriginalExpiryDate != null) {
+ long numDays = (date.getTimeInMillis() / 86400000) -
+ (mOriginalExpiryDate.getTimeInMillis() / 86400000);
+ if (numDays == 0) {
+ setExpiryDate(mOriginalExpiryDate);
+ } else {
+ setExpiryDate(date);
+ }
+ } else {
+ setExpiryDate(date);
+ }
+ if (mEditorListener != null) {
+ mEditorListener.onEdited();
+ }
+ }
+ }
+ };
+
+ 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 = (BootstrapButton) findViewById(R.id.expiry);
+
+ mDeleteButton = (BootstrapButton) findViewById(R.id.delete);
+ mDeleteButton.setOnClickListener(this);
+ mChkCertify = (CheckBox) findViewById(R.id.chkCertify);
+ mChkCertify.setOnCheckedChangeListener(mCheckChanged);
+ mChkSign = (CheckBox) findViewById(R.id.chkSign);
+ mChkSign.setOnCheckedChangeListener(mCheckChanged);
+ mChkEncrypt = (CheckBox) findViewById(R.id.chkEncrypt);
+ mChkEncrypt.setOnCheckedChangeListener(mCheckChanged);
+ mChkAuthenticate = (CheckBox) findViewById(R.id.chkAuthenticate);
+ mChkAuthenticate.setOnCheckedChangeListener(mCheckChanged);
+
+ setExpiryDate(null);
+
+ mExpiryDateButton.setOnClickListener(new OnClickListener() {
+ @TargetApi(11)
+ public void onClick(View v) {
+ GregorianCalendar date = mExpiryDate;
+ if (date == null) {
+ date = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
+ }
+ /*
+ * Using custom DatePickerDialog which overrides the setTitle because
+ * the DatePickerDialog title is buggy (unix warparound bug).
+ * See: https://code.google.com/p/android/issues/detail?id=49066
+ */
+ DatePickerDialog dialog = new ExpiryDatePickerDialog(getContext(),
+ mExpiryDateSetListener, date.get(Calendar.YEAR), date.get(Calendar.MONTH),
+ date.get(Calendar.DAY_OF_MONTH));
+ mDatePickerResultCount = 0;
+ dialog.setCancelable(true);
+ dialog.setButton(Dialog.BUTTON_NEGATIVE,
+ getContext().getString(R.string.btn_no_date),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ // Note: Ignore results after the first one - android sends multiples.
+ if (mDatePickerResultCount++ == 0) {
+ setExpiryDate(null);
+ if (mEditorListener != null) {
+ mEditorListener.onEdited();
+ }
+ }
+ }
+ });
+
+ // setCalendarViewShown() is supported from API 11 onwards.
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) {
+ // Hide calendarView in tablets because of the unix warparound bug.
+ dialog.getDatePicker().setCalendarViewShown(false);
+ }
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) {
+ if (dialog != null && mCreatedDate != null) {
+ dialog.getDatePicker()
+ .setMinDate(
+ mCreatedDate.getTime().getTime() + DateUtils.DAY_IN_MILLIS);
+ } else {
+ //When created date isn't available
+ dialog.getDatePicker().setMinDate(date.getTime().getTime() + DateUtils.DAY_IN_MILLIS);
+ }
+ }
+
+ dialog.show();
+ }
+ });
+
+ super.onFinishInflate();
+ }
+
+ public void setCanBeEdited(boolean canBeEdited) {
+ if (!canBeEdited) {
+ mDeleteButton.setVisibility(View.INVISIBLE);
+ mExpiryDateButton.setEnabled(false);
+ mChkSign.setEnabled(false); //certify is always disabled
+ mChkEncrypt.setEnabled(false);
+ mChkAuthenticate.setEnabled(false);
+ }
+ }
+
+ public void setValue(PGPSecretKey key, boolean isMasterKey, int usage, boolean isNewKey) {
+ mKey = key;
+
+ mIsMasterKey = isMasterKey;
+ if (mIsMasterKey) {
+ mDeleteButton.setVisibility(View.INVISIBLE);
+ }
+
+ mAlgorithm.setText(PgpKeyHelper.getAlgorithmInfo(key));
+ String keyIdStr = PgpKeyHelper.convertKeyIdToHex(key.getKeyID());
+ mKeyId.setText(keyIdStr);
+
+ Vector<Choice> choices = new Vector<Choice>();
+ boolean isElGamalKey = (key.getPublicKey().getAlgorithm() == PGPPublicKey.ELGAMAL_ENCRYPT);
+ boolean isDSAKey = (key.getPublicKey().getAlgorithm() == PGPPublicKey.DSA);
+ if (isElGamalKey) {
+ mChkSign.setVisibility(View.INVISIBLE);
+ TableLayout table = (TableLayout) findViewById(R.id.table_keylayout);
+ TableRow row = (TableRow) findViewById(R.id.row_sign);
+ table.removeView(row);
+ }
+ if (isDSAKey) {
+ mChkEncrypt.setVisibility(View.INVISIBLE);
+ TableLayout table = (TableLayout) findViewById(R.id.table_keylayout);
+ TableRow row = (TableRow) findViewById(R.id.row_encrypt);
+ table.removeView(row);
+ }
+ if (!mIsMasterKey) {
+ mChkCertify.setVisibility(View.INVISIBLE);
+ TableLayout table = (TableLayout) findViewById(R.id.table_keylayout);
+ TableRow row = (TableRow) findViewById(R.id.row_certify);
+ table.removeView(row);
+ } else {
+ TextView mLabelUsage2 = (TextView) findViewById(R.id.label_usage2);
+ mLabelUsage2.setVisibility(View.INVISIBLE);
+ }
+
+ int selectId = 0;
+ mIsNewKey = isNewKey;
+ if (isNewKey) {
+ mUsage = usage;
+ mChkCertify.setChecked((usage & KeyFlags.CERTIFY_OTHER) == KeyFlags.CERTIFY_OTHER);
+ mChkSign.setChecked((usage & KeyFlags.SIGN_DATA) == KeyFlags.SIGN_DATA);
+ mChkEncrypt.setChecked(((usage & KeyFlags.ENCRYPT_COMMS) == KeyFlags.ENCRYPT_COMMS) ||
+ ((usage & KeyFlags.ENCRYPT_STORAGE) == KeyFlags.ENCRYPT_STORAGE));
+ mChkAuthenticate.setChecked((usage & KeyFlags.AUTHENTICATION) == KeyFlags.AUTHENTICATION);
+ } else {
+ mUsage = PgpKeyHelper.getKeyUsage(key);
+ mOriginalUsage = mUsage;
+ if (key.isMasterKey()) {
+ mChkCertify.setChecked(PgpKeyHelper.isCertificationKey(key));
+ }
+ mChkSign.setChecked(PgpKeyHelper.isSigningKey(key));
+ mChkEncrypt.setChecked(PgpKeyHelper.isEncryptionKey(key));
+ mChkAuthenticate.setChecked(PgpKeyHelper.isAuthenticationKey(key));
+ }
+
+ GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
+ cal.setTime(PgpKeyHelper.getCreationDate(key));
+ setCreatedDate(cal);
+ cal = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
+ Date expiryDate = PgpKeyHelper.getExpiryDate(key);
+ if (expiryDate == null) {
+ setExpiryDate(null);
+ } else {
+ cal.setTime(PgpKeyHelper.getExpiryDate(key));
+ setExpiryDate(cal);
+ mOriginalExpiryDate = 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, mIsNewKey);
+ }
+ }
+ }
+
+ public void setEditorListener(EditorListener listener) {
+ mEditorListener = listener;
+ }
+
+ private void setCreatedDate(GregorianCalendar date) {
+ mCreatedDate = date;
+ if (date == null) {
+ mCreationDate.setText(getContext().getString(R.string.none));
+ } else {
+ mCreationDate.setText(DateFormat.getDateInstance().format(date.getTime()));
+ }
+ }
+
+ private void setExpiryDate(GregorianCalendar date) {
+ mExpiryDate = date;
+ if (date == null) {
+ mExpiryDateButton.setText(getContext().getString(R.string.none));
+ } else {
+ mExpiryDateButton.setText(DateFormat.getDateInstance().format(date.getTime()));
+ }
+ }
+
+ public GregorianCalendar getExpiryDate() {
+ return mExpiryDate;
+ }
+
+ public int getUsage() {
+ mUsage = (mUsage & ~KeyFlags.CERTIFY_OTHER) |
+ (mChkCertify.isChecked() ? KeyFlags.CERTIFY_OTHER : 0);
+ mUsage = (mUsage & ~KeyFlags.SIGN_DATA) |
+ (mChkSign.isChecked() ? KeyFlags.SIGN_DATA : 0);
+ mUsage = (mUsage & ~KeyFlags.ENCRYPT_COMMS) |
+ (mChkEncrypt.isChecked() ? KeyFlags.ENCRYPT_COMMS : 0);
+ mUsage = (mUsage & ~KeyFlags.ENCRYPT_STORAGE) |
+ (mChkEncrypt.isChecked() ? KeyFlags.ENCRYPT_STORAGE : 0);
+ mUsage = (mUsage & ~KeyFlags.AUTHENTICATION) |
+ (mChkAuthenticate.isChecked() ? KeyFlags.AUTHENTICATION : 0);
+
+ return mUsage;
+ }
+
+ public boolean needsSaving() {
+ if (mIsNewKey) {
+ return true;
+ }
+
+ boolean retval = (getUsage() != mOriginalUsage);
+
+ boolean dateChanged;
+ boolean mOEDNull = (mOriginalExpiryDate == null);
+ boolean mEDNull = (mExpiryDate == null);
+ if (mOEDNull != mEDNull) {
+ dateChanged = true;
+ } else {
+ if (mOEDNull) {
+ //both null, no change
+ dateChanged = false;
+ } else {
+ dateChanged = ((mExpiryDate.compareTo(mOriginalExpiryDate)) != 0);
+ }
+ }
+ retval |= dateChanged;
+
+ return retval;
+ }
+
+ public boolean getIsNewKey() {
+ return mIsNewKey;
+ }
+}
+
+class ExpiryDatePickerDialog extends DatePickerDialog {
+
+ public ExpiryDatePickerDialog(Context context, OnDateSetListener callBack,
+ int year, int monthOfYear, int dayOfMonth) {
+ super(context, callBack, year, monthOfYear, dayOfMonth);
+ }
+
+ //Set permanent title.
+ public void setTitle(CharSequence title) {
+ super.setTitle(getContext().getString(R.string.expiry_date_dialog_title));
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyServerEditor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyServerEditor.java
new file mode 100644
index 000000000..171763672
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyServerEditor.java
@@ -0,0 +1,82 @@
+/*
+ * 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 android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import com.beardedhen.androidbootstrap.BootstrapButton;
+import org.sufficientlysecure.keychain.R;
+
+public class KeyServerEditor extends LinearLayout implements Editor, OnClickListener {
+ private EditorListener mEditorListener = null;
+
+ BootstrapButton 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 = (BootstrapButton) 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, false);
+ }
+ }
+ }
+
+ @Override
+ public boolean needsSaving() {
+ return false;
+ }
+
+ public void setEditorListener(EditorListener listener) {
+ mEditorListener = listener;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SectionView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SectionView.java
new file mode 100644
index 000000000..fb59cd3b7
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SectionView.java
@@ -0,0 +1,429 @@
+/*
+ * 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 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.support.v7.app.ActionBarActivity;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import com.beardedhen.androidbootstrap.BootstrapButton;
+
+import org.spongycastle.openpgp.PGPKeyFlags;
+import org.spongycastle.openpgp.PGPSecretKey;
+
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.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.CreateKeyDialogFragment;
+import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment;
+import org.sufficientlysecure.keychain.ui.widget.Editor.EditorListener;
+import org.sufficientlysecure.keychain.util.Choice;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Vector;
+
+public class SectionView extends LinearLayout implements OnClickListener, EditorListener, Editor {
+ private LayoutInflater mInflater;
+ private BootstrapButton mPlusButton;
+ private ViewGroup mEditors;
+ private TextView mTitle;
+ private int mType = 0;
+ private EditorListener mEditorListener = null;
+
+ private Choice mNewKeyAlgorithmChoice;
+ private int mNewKeySize;
+ private boolean mOldItemDeleted = false;
+ private ArrayList<String> mDeletedIDs = new ArrayList<String>();
+ private ArrayList<PGPSecretKey> mDeletedKeys = new ArrayList<PGPSecretKey>();
+ private boolean mCanBeEdited = true;
+
+ private ActionBarActivity mActivity;
+
+ private ProgressDialogFragment mGeneratingDialog;
+
+ public void setEditorListener(EditorListener listener) {
+ mEditorListener = listener;
+ }
+
+ public SectionView(Context context) {
+ super(context);
+ mActivity = (ActionBarActivity) context;
+ }
+
+ public SectionView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mActivity = (ActionBarActivity) 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_user_ids);
+ break;
+ }
+
+ case Id.type.key: {
+ mTitle.setText(R.string.section_keys);
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ }
+
+ public void setCanBeEdited(boolean canBeEdited) {
+ mCanBeEdited = canBeEdited;
+ if (!mCanBeEdited) {
+ mPlusButton.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onFinishInflate() {
+ mInflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ setDrawingCacheEnabled(true);
+ setAlwaysDrawnWithCacheEnabled(true);
+
+ mPlusButton = (BootstrapButton) findViewById(R.id.plusbutton);
+ mPlusButton.setOnClickListener(this);
+
+ mEditors = (ViewGroup) findViewById(R.id.editors);
+ mTitle = (TextView) findViewById(R.id.title);
+
+ updateEditorsVisible();
+ super.onFinishInflate();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void onDeleted(Editor editor, boolean wasNewItem) {
+ mOldItemDeleted |= !wasNewItem;
+ if (mOldItemDeleted) {
+ if (mType == Id.type.user_id) {
+ mDeletedIDs.add(((UserIdEditor) editor).getOriginalID());
+ } else if (mType == Id.type.key) {
+ mDeletedKeys.add(((KeyEditor) editor).getValue());
+ }
+ }
+ this.updateEditorsVisible();
+ if (mEditorListener != null) {
+ mEditorListener.onEdited();
+ }
+ }
+
+ @Override
+ public void onEdited() {
+ if (mEditorListener != null) {
+ mEditorListener.onEdited();
+ }
+ }
+
+ protected void updateEditorsVisible() {
+ final boolean hasChildren = mEditors.getChildCount() > 0;
+ mEditors.setVisibility(hasChildren ? View.VISIBLE : View.GONE);
+ }
+
+ public boolean needsSaving() {
+ //check each view for needs saving, take account of deleted items
+ boolean ret = mOldItemDeleted;
+ for (int i = 0; i < mEditors.getChildCount(); ++i) {
+ Editor editor = (Editor) mEditors.getChildAt(i);
+ ret |= editor.needsSaving();
+ if (mType == Id.type.user_id) {
+ ret |= ((UserIdEditor) editor).primarySwapped();
+ }
+ }
+ return ret;
+ }
+
+ public boolean primaryChanged() {
+ boolean ret = false;
+ for (int i = 0; i < mEditors.getChildCount(); ++i) {
+ Editor editor = (Editor) mEditors.getChildAt(i);
+ if (mType == Id.type.user_id) {
+ ret |= ((UserIdEditor) editor).primarySwapped();
+ }
+ }
+ return ret;
+ }
+
+ public String getOriginalPrimaryID() {
+ //NB: this will have to change when we change how Primary IDs are chosen, and so we need to be
+ // careful about where Master key capabilities are stored... multiple primaries and
+ // revoked ones make this harder than the simple case we are continuing to assume here
+ for (int i = 0; i < mEditors.getChildCount(); ++i) {
+ Editor editor = (Editor) mEditors.getChildAt(i);
+ if (mType == Id.type.user_id) {
+ if (((UserIdEditor) editor).getIsOriginallyMainUserID()) {
+ return ((UserIdEditor) editor).getOriginalID();
+ }
+ }
+ }
+ return null;
+ }
+
+ public ArrayList<String> getOriginalIDs() {
+ ArrayList<String> orig = new ArrayList<String>();
+ if (mType == Id.type.user_id) {
+ for (int i = 0; i < mEditors.getChildCount(); ++i) {
+ UserIdEditor editor = (UserIdEditor) mEditors.getChildAt(i);
+ if (editor.isMainUserId()) {
+ orig.add(0, editor.getOriginalID());
+ } else {
+ orig.add(editor.getOriginalID());
+ }
+ }
+ return orig;
+ } else {
+ return null;
+ }
+ }
+
+ public ArrayList<String> getDeletedIDs() {
+ return mDeletedIDs;
+ }
+
+ public ArrayList<PGPSecretKey> getDeletedKeys() {
+ return mDeletedKeys;
+ }
+
+ public List<Boolean> getNeedsSavingArray() {
+ ArrayList<Boolean> mList = new ArrayList<Boolean>();
+ for (int i = 0; i < mEditors.getChildCount(); ++i) {
+ Editor editor = (Editor) mEditors.getChildAt(i);
+ mList.add(editor.needsSaving());
+ }
+ return mList;
+ }
+
+ public List<Boolean> getNewIDFlags() {
+ ArrayList<Boolean> mList = new ArrayList<Boolean>();
+ for (int i = 0; i < mEditors.getChildCount(); ++i) {
+ UserIdEditor editor = (UserIdEditor) mEditors.getChildAt(i);
+ if (editor.isMainUserId()) {
+ mList.add(0, editor.getIsNewID());
+ } else {
+ mList.add(editor.getIsNewID());
+ }
+ }
+ return mList;
+ }
+
+ public List<Boolean> getNewKeysArray() {
+ ArrayList<Boolean> mList = new ArrayList<Boolean>();
+ if (mType == Id.type.key) {
+ for (int i = 0; i < mEditors.getChildCount(); ++i) {
+ KeyEditor editor = (KeyEditor) mEditors.getChildAt(i);
+ mList.add(editor.getIsNewKey());
+ }
+ }
+ return mList;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void onClick(View v) {
+ if (mCanBeEdited) {
+ switch (mType) {
+ case Id.type.user_id: {
+ UserIdEditor view = (UserIdEditor) mInflater.inflate(
+ R.layout.edit_key_user_id_item, mEditors, false);
+ view.setEditorListener(this);
+ view.setValue("", mEditors.getChildCount() == 0, true);
+ mEditors.addView(view);
+ if (mEditorListener != null) {
+ mEditorListener.onEdited();
+ }
+ break;
+ }
+
+ case Id.type.key: {
+ CreateKeyDialogFragment mCreateKeyDialogFragment =
+ CreateKeyDialogFragment.newInstance(mEditors.getChildCount());
+ mCreateKeyDialogFragment
+ .setOnAlgorithmSelectedListener(
+ new CreateKeyDialogFragment.OnAlgorithmSelectedListener() {
+ @Override
+ public void onAlgorithmSelected(Choice algorithmChoice, int keySize) {
+ mNewKeyAlgorithmChoice = algorithmChoice;
+ mNewKeySize = keySize;
+ createKey();
+ }
+ });
+ mCreateKeyDialogFragment.show(mActivity.getSupportFragmentManager(), "createKeyDialog");
+ 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, mEditors.getChildCount() == 0, false);
+ view.setCanBeEdited(mCanBeEdited);
+ mEditors.addView(view);
+ }
+
+ this.updateEditorsVisible();
+ }
+
+ public void setKeys(Vector<PGPSecretKey> list, Vector<Integer> usages, boolean newKeys) {
+ 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), newKeys);
+ view.setCanBeEdited(mCanBeEdited);
+ mEditors.addView(view);
+ }
+
+ this.updateEditorsVisible();
+ }
+
+ private void createKey() {
+ // Send all information needed to service to edit key in other thread
+ final Intent intent = new Intent(mActivity, KeychainIntentService.class);
+
+ intent.setAction(KeychainIntentService.ACTION_GENERATE_KEY);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+ Boolean isMasterKey;
+
+ String passphrase;
+ if (mEditors.getChildCount() > 0) {
+ PGPSecretKey masterKey = ((KeyEditor) mEditors.getChildAt(0)).getValue();
+ passphrase = PassphraseCacheService
+ .getCachedPassphrase(mActivity, masterKey.getKeyID());
+ isMasterKey = false;
+ } else {
+ passphrase = "";
+ isMasterKey = true;
+ }
+ data.putBoolean(KeychainIntentService.GENERATE_KEY_MASTER_KEY, isMasterKey);
+ 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(
+ getResources().getQuantityString(R.plurals.progress_generating, 1),
+ ProgressDialog.STYLE_SPINNER,
+ true,
+ new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ mActivity.stopService(intent);
+ }
+ });
+
+ // Message is received after generating is done in KeychainIntentService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(mActivity,
+ mGeneratingDialog) {
+ public void handleMessage(Message message) {
+ // handle messages by standard KeychainIntentServiceHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ // get new key from data bundle returned from service
+ Bundle data = message.getData();
+ PGPSecretKey newKey = (PGPSecretKey) PgpConversionHelper
+ .BytesToPGPSecretKey(data
+ .getByteArray(KeychainIntentService.RESULT_NEW_KEY));
+ addGeneratedKeyToView(newKey);
+ }
+ }
+ };
+
+ // 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);
+ }
+
+ private void addGeneratedKeyToView(PGPSecretKey newKey) {
+ // add view with new key
+ KeyEditor view = (KeyEditor) mInflater.inflate(R.layout.edit_key_key_item,
+ mEditors, false);
+ view.setEditorListener(SectionView.this);
+ int usage = 0;
+ if (mEditors.getChildCount() == 0) {
+ usage = PGPKeyFlags.CAN_CERTIFY;
+ }
+ view.setValue(newKey, newKey.isMasterKey(), usage, true);
+ mEditors.addView(view);
+ SectionView.this.updateEditorsVisible();
+ if (mEditorListener != null) {
+ mEditorListener.onEdited();
+ }
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/UnderlineTextView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/UnderlineTextView.java
new file mode 100644
index 000000000..937a48e48
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/UnderlineTextView.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2013 Eric Frohnhoefer
+ *
+ * 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.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.widget.TextView;
+
+/**
+ * Copied from StickyListHeaders lib example
+ *
+ * @author Eric Frohnhoefer
+ */
+public class UnderlineTextView extends TextView {
+ private final Paint mPaint = new Paint();
+ private int mUnderlineHeight = 0;
+
+ public UnderlineTextView(Context context) {
+ this(context, null);
+ }
+
+ public UnderlineTextView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public UnderlineTextView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ init(context, attrs);
+ }
+
+ private void init(Context context, AttributeSet attrs) {
+ Resources r = getResources();
+ mUnderlineHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2,
+ r.getDisplayMetrics());
+ }
+
+ @Override
+ public void setPadding(int left, int top, int right, int bottom) {
+ super.setPadding(left, top, right, bottom + mUnderlineHeight);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ // Draw the underline the same color as the text
+ mPaint.setColor(getTextColors().getDefaultColor());
+ canvas.drawRect(0, getHeight() - mUnderlineHeight, getWidth(), getHeight(), mPaint);
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/UserIdEditor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/UserIdEditor.java
new file mode 100644
index 000000000..2253872d5
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/UserIdEditor.java
@@ -0,0 +1,265 @@
+/*
+ * 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 android.content.Context;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.util.Patterns;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.AutoCompleteTextView;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.RadioButton;
+import com.beardedhen.androidbootstrap.BootstrapButton;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.helper.ContactHelper;
+import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
+
+import java.util.regex.Matcher;
+
+public class UserIdEditor extends LinearLayout implements Editor, OnClickListener {
+ private EditorListener mEditorListener = null;
+
+ private BootstrapButton mDeleteButton;
+ private RadioButton mIsMainUserId;
+ private String mOriginalID;
+ private EditText mName;
+ private String mOriginalName;
+ private AutoCompleteTextView mEmail;
+ private String mOriginalEmail;
+ private EditText mComment;
+ private String mOriginalComment;
+ private boolean mOriginallyMainUserID;
+ private boolean mIsNewId;
+
+ public void setCanBeEdited(boolean canBeEdited) {
+ if (!canBeEdited) {
+ mDeleteButton.setVisibility(View.INVISIBLE);
+ mName.setEnabled(false);
+ mIsMainUserId.setEnabled(false);
+ mEmail.setEnabled(false);
+ mComment.setEnabled(false);
+ }
+ }
+
+ 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);
+ }
+
+ TextWatcher mTextWatcher = new TextWatcher() {
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (mEditorListener != null) {
+ mEditorListener.onEdited();
+ }
+ }
+ };
+
+ @Override
+ protected void onFinishInflate() {
+ setDrawingCacheEnabled(true);
+ setAlwaysDrawnWithCacheEnabled(true);
+
+ mDeleteButton = (BootstrapButton) findViewById(R.id.delete);
+ mDeleteButton.setOnClickListener(this);
+ mIsMainUserId = (RadioButton) findViewById(R.id.isMainUserId);
+ mIsMainUserId.setOnClickListener(this);
+
+ mName = (EditText) findViewById(R.id.name);
+ mName.addTextChangedListener(mTextWatcher);
+ mEmail = (AutoCompleteTextView) findViewById(R.id.email);
+ mComment = (EditText) findViewById(R.id.comment);
+ mComment.addTextChangedListener(mTextWatcher);
+
+
+ mEmail.setThreshold(1); // Start working from first character
+ mEmail.setAdapter(
+ new ArrayAdapter<String>
+ (this.getContext(), android.R.layout.simple_dropdown_item_1line,
+ ContactHelper.getMailAccounts(getContext())
+ ));
+ mEmail.addTextChangedListener(new TextWatcher(){
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ String email = editable.toString();
+ if (email.length() > 0) {
+ Matcher emailMatcher = Patterns.EMAIL_ADDRESS.matcher(email);
+ if (emailMatcher.matches()) {
+ mEmail.setCompoundDrawablesWithIntrinsicBounds(0, 0,
+ android.R.drawable.presence_online, 0);
+ } else {
+ mEmail.setCompoundDrawablesWithIntrinsicBounds(0, 0,
+ android.R.drawable.presence_offline, 0);
+ }
+ } else {
+ // remove drawable if email is empty
+ mEmail.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
+ }
+ if (mEditorListener != null) {
+ mEditorListener.onEdited();
+ }
+ }
+ });
+
+ super.onFinishInflate();
+ }
+
+ public void setValue(String userId, boolean isMainID, boolean isNewId) {
+
+ mName.setText("");
+ mOriginalName = "";
+ mComment.setText("");
+ mOriginalComment = "";
+ mEmail.setText("");
+ mOriginalEmail = "";
+ mIsNewId = isNewId;
+ mOriginalID = userId;
+
+ String[] result = PgpKeyHelper.splitUserId(userId);
+ if (result[0] != null) {
+ mName.setText(result[0]);
+ mOriginalName = result[0];
+ }
+ if (result[1] != null) {
+ mEmail.setText(result[1]);
+ mOriginalEmail = result[1];
+ }
+ if (result[2] != null) {
+ mComment.setText(result[2]);
+ mOriginalComment = result[2];
+ }
+
+ mOriginallyMainUserID = isMainID;
+ setIsMainUserId(isMainID);
+ }
+
+ public String getValue() {
+ String name = ("" + mName.getText()).trim();
+ String email = ("" + mEmail.getText()).trim();
+ String comment = ("" + mComment.getText()).trim();
+
+ String userId = name;
+ if (comment.length() > 0) {
+ userId += " (" + comment + ")";
+ }
+ if (email.length() > 0) {
+ userId += " <" + email + ">";
+ }
+
+ if (userId.equals("")) {
+ // ok, empty one...
+ return userId;
+ }
+ //TODO: check gpg accepts an entirely empty ID packet. specs say this is allowed
+ 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, mIsNewId);
+ }
+ 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);
+ }
+ }
+ if (mEditorListener != null) {
+ mEditorListener.onEdited();
+ }
+ }
+ }
+
+ public void setIsMainUserId(boolean value) {
+ mIsMainUserId.setChecked(value);
+ }
+
+ public boolean isMainUserId() {
+ return mIsMainUserId.isChecked();
+ }
+
+ public void setEditorListener(EditorListener listener) {
+ mEditorListener = listener;
+ }
+
+ @Override
+ public boolean needsSaving() {
+ boolean retval = false; //(mOriginallyMainUserID != isMainUserId());
+ retval |= !(mOriginalName.equals(("" + mName.getText()).trim()));
+ retval |= !(mOriginalEmail.equals(("" + mEmail.getText()).trim()));
+ retval |= !(mOriginalComment.equals(("" + mComment.getText()).trim()));
+ retval |= mIsNewId;
+ return retval;
+ }
+
+ public boolean getIsOriginallyMainUserID() {
+ return mOriginallyMainUserID;
+ }
+
+ public boolean primarySwapped() {
+ return (mOriginallyMainUserID != isMainUserId());
+ }
+
+ public String getOriginalID() {
+ return mOriginalID;
+ }
+
+ public boolean getIsNewID() { return mIsNewId; }
+}