aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--OpenKeychain/src/main/AndroidManifest.xml8
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java39
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/EditKeyOperation.java25
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java403
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java2
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/UploadOperation.java162
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ExportResult.java21
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java5
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/UploadResult.java73
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java33
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java9
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java37
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ExportKeyringParcel.java48
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java3
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/UploadKeyringParcel.java79
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupActivity.java77
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeFragment.java550
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java22
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java23
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DrawerBackupFragment.java (renamed from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupFragment.java)37
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java6
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java248
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java4
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java36
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java47
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java5
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Notify.java7
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java26
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java2
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java39
-rw-r--r--OpenKeychain/src/main/res/anim/fade_in_delayed.xml8
-rw-r--r--OpenKeychain/src/main/res/anim/fade_out_delayed.xml8
-rw-r--r--OpenKeychain/src/main/res/layout-land/backup_code_fragment.xml364
-rw-r--r--OpenKeychain/src/main/res/layout-land/qr_code_activity.xml39
-rw-r--r--OpenKeychain/src/main/res/layout-mdpi/backup_code_fragment.xml363
-rw-r--r--OpenKeychain/src/main/res/layout-xhdpi/backup_code_fragment.xml364
-rw-r--r--OpenKeychain/src/main/res/layout/backup_activity.xml26
-rw-r--r--OpenKeychain/src/main/res/layout/decrypt_list_entry.xml34
-rw-r--r--OpenKeychain/src/main/res/layout/drawer_backup_fragment.xml (renamed from OpenKeychain/src/main/res/layout/backup_fragment.xml)0
-rw-r--r--OpenKeychain/src/main/res/layout/import_keys_activity.xml29
-rw-r--r--OpenKeychain/src/main/res/menu/key_view.xml2
-rw-r--r--OpenKeychain/src/main/res/values/strings.xml22
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/ExportTest.java232
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/TestingUtils.java24
44 files changed, 2854 insertions, 737 deletions
diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml
index e80faff58..2f31d6d63 100644
--- a/OpenKeychain/src/main/AndroidManifest.xml
+++ b/OpenKeychain/src/main/AndroidManifest.xml
@@ -439,6 +439,14 @@
android:label="@string/title_key_server_preference"
android:windowSoftInputMode="stateHidden" />
<activity
+ android:name=".ui.BackupActivity"
+ android:configChanges="keyboardHidden|keyboard"
+ android:label="@string/title_backup">
+ <meta-data
+ android:name="android.support.PARENT_ACTIVITY"
+ android:value=".ui.MainActivity" />
+ </activity>
+ <activity
android:name=".ui.CertifyKeyActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/title_certify_key">
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java
index eeed24db0..e1daac874 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java
@@ -17,15 +17,20 @@
package org.sufficientlysecure.keychain.operations;
+
+import java.net.Proxy;
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+
import android.content.Context;
import android.support.annotation.NonNull;
import org.sufficientlysecure.keychain.keyimport.HkpKeyserver;
import org.sufficientlysecure.keychain.operations.results.CertifyResult;
-import org.sufficientlysecure.keychain.operations.results.ExportResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult;
+import org.sufficientlysecure.keychain.operations.results.UploadResult;
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing;
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing;
@@ -40,6 +45,7 @@ import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException
import org.sufficientlysecure.keychain.service.CertifyActionsParcel;
import org.sufficientlysecure.keychain.service.CertifyActionsParcel.CertifyAction;
import org.sufficientlysecure.keychain.service.ContactSyncAdapterService;
+import org.sufficientlysecure.keychain.service.UploadKeyringParcel;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.NfcSignOperationsBuilder;
@@ -48,10 +54,6 @@ import org.sufficientlysecure.keychain.util.Passphrase;
import org.sufficientlysecure.keychain.util.Preferences;
import org.sufficientlysecure.keychain.util.orbot.OrbotHelper;
-import java.net.Proxy;
-import java.util.ArrayList;
-import java.util.concurrent.atomic.AtomicBoolean;
-
/**
* An operation which implements a high level user id certification operation.
* <p/>
@@ -204,23 +206,9 @@ public class CertifyOperation extends BaseOperation<CertifyActionsParcel> {
}
// these variables are used inside the following loop, but they need to be created only once
- HkpKeyserver keyServer = null;
- ExportOperation exportOperation = null;
- Proxy proxy = null;
+ UploadOperation uploadOperation = null;
if (parcel.keyServerUri != null) {
- keyServer = new HkpKeyserver(parcel.keyServerUri);
- exportOperation = new ExportOperation(mContext, mProviderHelper, mProgressable);
- if (cryptoInput.getParcelableProxy() == null) {
- // explicit proxy not set
- if (!OrbotHelper.isOrbotInRequiredState(mContext)) {
- return new CertifyResult(null,
- RequiredInputParcel.createOrbotRequiredOperation(), cryptoInput);
- }
- proxy = Preferences.getPreferences(mContext).getProxyPrefs()
- .parcelableProxy.getProxy();
- } else {
- proxy = cryptoInput.getParcelableProxy().getProxy();
- }
+ uploadOperation = new UploadOperation(mContext, mProviderHelper, mProgressable);
}
// Write all certified keys into the database
@@ -239,11 +227,10 @@ public class CertifyOperation extends BaseOperation<CertifyActionsParcel> {
mProviderHelper.clearLog();
SaveKeyringResult result = mProviderHelper.savePublicKeyRing(certifiedKey);
- if (exportOperation != null) {
- ExportResult uploadResult = exportOperation.uploadKeyRingToServer(
- keyServer,
- certifiedKey,
- proxy);
+ if (uploadOperation != null) {
+ UploadKeyringParcel uploadInput =
+ new UploadKeyringParcel(parcel.keyServerUri, certifiedKey.getMasterKeyId());
+ UploadResult uploadResult = uploadOperation.execute(uploadInput, cryptoInput);
log.add(uploadResult, 2);
if (uploadResult.success()) {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/EditKeyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/EditKeyOperation.java
index f5ba88502..cf8928768 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/EditKeyOperation.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/EditKeyOperation.java
@@ -17,17 +17,21 @@
package org.sufficientlysecure.keychain.operations;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
import android.content.Context;
import android.support.annotation.NonNull;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.EditKeyResult;
-import org.sufficientlysecure.keychain.operations.results.ExportResult;
import org.sufficientlysecure.keychain.operations.results.InputPendingResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult;
import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult;
+import org.sufficientlysecure.keychain.operations.results.UploadResult;
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing;
import org.sufficientlysecure.keychain.pgp.PgpKeyOperation;
import org.sufficientlysecure.keychain.pgp.Progressable;
@@ -35,17 +39,14 @@ import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException;
import org.sufficientlysecure.keychain.service.ContactSyncAdapterService;
-import org.sufficientlysecure.keychain.service.ExportKeyringParcel;
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
+import org.sufficientlysecure.keychain.service.UploadKeyringParcel;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.ProgressScaler;
-import java.io.IOException;
-import java.util.concurrent.atomic.AtomicBoolean;
-
/**
* An operation which implements a high level key edit operation.
* <p/>
@@ -134,20 +135,20 @@ public class EditKeyOperation extends BaseOperation<SaveKeyringParcel> {
UncachedKeyRing ring = modifyResult.getRing();
if (saveParcel.isUpload()) {
- UncachedKeyRing publicKeyRing;
+ byte[] keyringBytes;
try {
- publicKeyRing = ring.extractPublicKeyRing();
+ UncachedKeyRing publicKeyRing = ring.extractPublicKeyRing();
+ keyringBytes = publicKeyRing.getEncoded();
} catch (IOException e) {
log.add(LogType.MSG_ED_ERROR_EXTRACTING_PUBLIC_UPLOAD, 1);
return new EditKeyResult(EditKeyResult.RESULT_ERROR, log, null);
}
- ExportKeyringParcel exportKeyringParcel =
- new ExportKeyringParcel(saveParcel.getUploadKeyserver(),
- publicKeyRing);
+ UploadKeyringParcel exportKeyringParcel =
+ new UploadKeyringParcel(saveParcel.getUploadKeyserver(), keyringBytes);
- ExportResult uploadResult =
- new ExportOperation(mContext, mProviderHelper, mProgressable)
+ UploadResult uploadResult =
+ new UploadOperation(mContext, mProviderHelper, mProgressable)
.execute(exportKeyringParcel, cryptoInput);
if (uploadResult.isPending()) {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java
index 531ac01f2..ecff9f5ae 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java
@@ -18,47 +18,49 @@
package org.sufficientlysecure.keychain.operations;
+
import java.io.BufferedOutputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
+import java.io.DataOutputStream;
import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.io.OutputStream;
-import java.net.Proxy;
+import java.text.SimpleDateFormat;
import java.util.Collections;
+import java.util.Date;
+import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.text.TextUtils;
import org.spongycastle.bcpg.ArmoredOutputStream;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
-import org.sufficientlysecure.keychain.keyimport.HkpKeyserver;
-import org.sufficientlysecure.keychain.keyimport.Keyserver.AddKeyException;
import org.sufficientlysecure.keychain.operations.results.ExportResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+import org.sufficientlysecure.keychain.operations.results.PgpSignEncryptResult;
import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing;
-import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing;
+import org.sufficientlysecure.keychain.pgp.PgpSignEncryptInputParcel;
+import org.sufficientlysecure.keychain.pgp.PgpSignEncryptOperation;
import org.sufficientlysecure.keychain.pgp.Progressable;
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider;
import org.sufficientlysecure.keychain.service.ExportKeyringParcel;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
-import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
-import org.sufficientlysecure.keychain.util.FileHelper;
+import org.sufficientlysecure.keychain.util.InputData;
import org.sufficientlysecure.keychain.util.Log;
-import org.sufficientlysecure.keychain.util.Preferences;
-import org.sufficientlysecure.keychain.util.orbot.OrbotHelper;
+
/**
* An operation class which implements high level export
@@ -72,6 +74,17 @@ import org.sufficientlysecure.keychain.util.orbot.OrbotHelper;
*/
public class ExportOperation extends BaseOperation<ExportKeyringParcel> {
+ private static final String[] PROJECTION = new String[] {
+ KeyRings.MASTER_KEY_ID,
+ KeyRings.PUBKEY_DATA,
+ KeyRings.PRIVKEY_DATA,
+ KeyRings.HAS_ANY_SECRET
+ };
+ private static final int INDEX_MASTER_KEY_ID = 0;
+ private static final int INDEX_PUBKEY_DATA = 1;
+ private static final int INDEX_SECKEY_DATA = 2;
+ private static final int INDEX_HAS_ANY_SECRET = 3;
+
public ExportOperation(Context context, ProviderHelper providerHelper, Progressable
progressable) {
super(context, providerHelper, progressable);
@@ -82,234 +95,129 @@ public class ExportOperation extends BaseOperation<ExportKeyringParcel> {
super(context, providerHelper, progressable, cancelled);
}
- public ExportResult uploadKeyRingToServer(HkpKeyserver server, CanonicalizedPublicKeyRing keyring,
- Proxy proxy) {
- return uploadKeyRingToServer(server, keyring.getUncachedKeyRing(), proxy);
- }
-
- public ExportResult uploadKeyRingToServer(HkpKeyserver server, UncachedKeyRing keyring, Proxy proxy) {
- mProgressable.setProgress(R.string.progress_uploading, 0, 1);
+ @NonNull
+ public ExportResult execute(@NonNull ExportKeyringParcel exportInput, @Nullable CryptoInputParcel cryptoInput) {
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
- ArmoredOutputStream aos = null;
OperationLog log = new OperationLog();
- log.add(LogType.MSG_EXPORT_UPLOAD_PUBLIC, 0, KeyFormattingUtils.convertKeyIdToHex(
- keyring.getPublicKey().getKeyId()
- ));
+ if (exportInput.mMasterKeyIds != null) {
+ log.add(LogType.MSG_EXPORT, 0, exportInput.mMasterKeyIds.length);
+ } else {
+ log.add(LogType.MSG_EXPORT_ALL, 0);
+ }
try {
- aos = new ArmoredOutputStream(bos);
- keyring.encode(aos);
- aos.close();
-
- String armoredKey = bos.toString("UTF-8");
- server.add(armoredKey, proxy);
- log.add(LogType.MSG_EXPORT_UPLOAD_SUCCESS, 1);
- return new ExportResult(ExportResult.RESULT_OK, log);
- } catch (IOException e) {
- Log.e(Constants.TAG, "IOException", e);
+ boolean nonEncryptedOutput = exportInput.mSymmetricPassphrase == null;
- log.add(LogType.MSG_EXPORT_ERROR_KEY, 1);
- return new ExportResult(ExportResult.RESULT_ERROR, log);
- } catch (AddKeyException e) {
- Log.e(Constants.TAG, "AddKeyException", e);
+ Uri exportOutputUri = nonEncryptedOutput
+ ? exportInput.mOutputUri
+ : TemporaryStorageProvider.createFile(mContext);
- log.add(LogType.MSG_EXPORT_ERROR_UPLOAD, 1);
- return new ExportResult(ExportResult.RESULT_ERROR, log);
- } finally {
- mProgressable.setProgress(R.string.progress_uploading, 1, 1);
- try {
- if (aos != null) {
- aos.close();
- }
- bos.close();
- } catch (IOException e) {
- // this is just a finally thing, no matter if it doesn't work out.
- }
- }
- }
+ int exportedDataSize;
- public ExportResult exportToFile(long[] masterKeyIds, boolean exportSecret, String outputFile) {
+ { // export key data, and possibly return if we don't encrypt
- OperationLog log = new OperationLog();
- if (masterKeyIds != null) {
- log.add(LogType.MSG_EXPORT, 0, masterKeyIds.length);
- } else {
- log.add(LogType.MSG_EXPORT_ALL, 0);
- }
+ DataOutputStream outStream = new DataOutputStream(new BufferedOutputStream(
+ mContext.getContentResolver().openOutputStream(exportOutputUri)));
- // do we have a file name?
- if (outputFile == null) {
- log.add(LogType.MSG_EXPORT_ERROR_NO_FILE, 1);
- return new ExportResult(ExportResult.RESULT_ERROR, log);
- }
+ boolean exportSuccess = exportKeysToStream(
+ log, exportInput.mMasterKeyIds, exportInput.mExportSecret, outStream);
- log.add(LogType.MSG_EXPORT_FILE_NAME, 1, outputFile);
+ exportedDataSize = outStream.size();
- // check if storage is ready
- if (!FileHelper.isStorageMounted(outputFile)) {
- log.add(LogType.MSG_EXPORT_ERROR_STORAGE, 1);
- return new ExportResult(ExportResult.RESULT_ERROR, log);
- }
+ if (!exportSuccess) {
+ // if there was an error, it will be in the log so we just have to return
+ return new ExportResult(ExportResult.RESULT_ERROR, log);
+ }
- try {
- OutputStream outStream = new FileOutputStream(outputFile);
- try {
- ExportResult result = exportKeyRings(log, masterKeyIds, exportSecret, outStream);
- if (result.cancelled()) {
- //noinspection ResultOfMethodCallIgnored
- new File(outputFile).delete();
+ if (nonEncryptedOutput) {
+ // log.add(LogType.MSG_EXPORT_NO_ENCRYPT, 1);
+ log.add(LogType.MSG_EXPORT_SUCCESS, 1);
+ return new ExportResult(ExportResult.RESULT_OK, log);
}
- return result;
- } finally {
- outStream.close();
}
- } catch (IOException e) {
- log.add(LogType.MSG_EXPORT_ERROR_FOPEN, 1);
- return new ExportResult(ExportResult.RESULT_ERROR, log);
- }
- }
+ PgpSignEncryptOperation pseOp = new PgpSignEncryptOperation(mContext, mProviderHelper, mProgressable, mCancelled);
- public ExportResult exportToUri(long[] masterKeyIds, boolean exportSecret, Uri outputUri) {
+ PgpSignEncryptInputParcel inputParcel = new PgpSignEncryptInputParcel();
+ inputParcel.setSymmetricPassphrase(exportInput.mSymmetricPassphrase);
+ inputParcel.setEnableAsciiArmorOutput(true);
- OperationLog log = new OperationLog();
- if (masterKeyIds != null) {
- log.add(LogType.MSG_EXPORT, 0, masterKeyIds.length);
- } else {
- log.add(LogType.MSG_EXPORT_ALL, 0);
- }
+ InputStream inStream = mContext.getContentResolver().openInputStream(exportOutputUri);
- // do we have a file name?
- if (outputUri == null) {
- log.add(LogType.MSG_EXPORT_ERROR_NO_URI, 1);
- return new ExportResult(ExportResult.RESULT_ERROR, log);
- }
+ String filename;
+ if (exportInput.mMasterKeyIds != null && exportInput.mMasterKeyIds.length == 1) {
+ filename = "backup_" + KeyFormattingUtils.convertKeyIdToHex(exportInput.mMasterKeyIds[0]);
+ filename += exportInput.mExportSecret ? ".sec.asc" : ".pub.asc";
+ } else {
+ filename = "backup_" + new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date());
+ filename += exportInput.mExportSecret ? ".asc" : ".pub.asc";
+ }
- try {
- OutputStream outStream = mProviderHelper.getContentResolver().openOutputStream
- (outputUri);
- return exportKeyRings(log, masterKeyIds, exportSecret, outStream);
- } catch (FileNotFoundException e) {
- log.add(LogType.MSG_EXPORT_ERROR_URI_OPEN, 1);
- return new ExportResult(ExportResult.RESULT_ERROR, log);
- }
+ InputData inputData = new InputData(inStream, exportedDataSize, filename);
- }
+ OutputStream outStream = mContext.getContentResolver().openOutputStream(exportInput.mOutputUri);
+ outStream = new BufferedOutputStream(outStream);
+
+ PgpSignEncryptResult encryptResult = pseOp.execute(inputParcel, new CryptoInputParcel(), inputData, outStream);
+ if (!encryptResult.success()) {
+ log.addByMerge(encryptResult, 1);
+ // log.add(LogType.MSG_EXPORT_ERROR_ENCRYPT, 1);
+ return new ExportResult(ExportResult.RESULT_ERROR, log);
+ }
- ExportResult exportKeyRings(OperationLog log, long[] masterKeyIds, boolean exportSecret,
- OutputStream outStream) {
+ log.add(encryptResult, 1);
+ log.add(LogType.MSG_EXPORT_SUCCESS, 1);
+ return new ExportResult(ExportResult.RESULT_OK, log);
- /* TODO isn't this checked above, with the isStorageMounted call?
- if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
- log.add(LogType.MSG_EXPORT_ERROR_STORAGE, 1);
+ } catch (FileNotFoundException e) {
+ log.add(LogType.MSG_EXPORT_ERROR_URI_OPEN, 1);
return new ExportResult(ExportResult.RESULT_ERROR, log);
- }
- */
- if (!BufferedOutputStream.class.isInstance(outStream)) {
- outStream = new BufferedOutputStream(outStream);
}
- int okSecret = 0, okPublic = 0, progress = 0;
-
- Cursor cursor = null;
- try {
+ }
- String selection = null, selectionArgs[] = null;
+ boolean exportKeysToStream(OperationLog log, long[] masterKeyIds, boolean exportSecret, OutputStream outStream) {
- if (masterKeyIds != null) {
- // convert long[] to String[]
- selectionArgs = new String[masterKeyIds.length];
- for (int i = 0; i < masterKeyIds.length; i++) {
- selectionArgs[i] = Long.toString(masterKeyIds[i]);
- }
+ // noinspection unused TODO use these in a log entry
+ int okSecret = 0, okPublic = 0;
- // generates ?,?,? as placeholders for selectionArgs
- String placeholders = TextUtils.join(",",
- Collections.nCopies(masterKeyIds.length, "?"));
+ int progress = 0;
- // put together selection string
- selection = Tables.KEYS + "." + KeyRings.MASTER_KEY_ID
- + " IN (" + placeholders + ")";
- }
+ Cursor cursor = queryForKeys(masterKeyIds);
- cursor = mProviderHelper.getContentResolver().query(
- KeyRings.buildUnifiedKeyRingsUri(), new String[]{
- KeyRings.MASTER_KEY_ID, KeyRings.PUBKEY_DATA,
- KeyRings.PRIVKEY_DATA, KeyRings.HAS_ANY_SECRET
- }, selection, selectionArgs, Tables.KEYS + "." + KeyRings.MASTER_KEY_ID
- );
+ if (cursor == null || !cursor.moveToFirst()) {
+ log.add(LogType.MSG_EXPORT_ERROR_DB, 1);
+ return false; // new ExportResult(ExportResult.RESULT_ERROR, log);
+ }
- if (cursor == null || !cursor.moveToFirst()) {
- log.add(LogType.MSG_EXPORT_ERROR_DB, 1);
- return new ExportResult(ExportResult.RESULT_ERROR, log, okPublic, okSecret);
- }
+ try {
int numKeys = cursor.getCount();
- updateProgress(
- mContext.getResources().getQuantityString(R.plurals.progress_exporting_key,
- numKeys), 0, numKeys);
+ updateProgress(mContext.getResources().getQuantityString(R.plurals.progress_exporting_key, numKeys),
+ 0, numKeys);
// For each public masterKey id
while (!cursor.isAfterLast()) {
- long keyId = cursor.getLong(0);
- ArmoredOutputStream arOutStream = null;
-
- // Create an output stream
- try {
- arOutStream = new ArmoredOutputStream(outStream);
-
- log.add(LogType.MSG_EXPORT_PUBLIC, 1, KeyFormattingUtils.beautifyKeyId(keyId));
-
- byte[] data = cursor.getBlob(1);
- CanonicalizedKeyRing ring =
- UncachedKeyRing.decodeFromData(data).canonicalize(log, 2, true);
- ring.encode(arOutStream);
+ long keyId = cursor.getLong(INDEX_MASTER_KEY_ID);
+ log.add(LogType.MSG_EXPORT_PUBLIC, 1, KeyFormattingUtils.beautifyKeyId(keyId));
+ if (writePublicKeyToStream(log, outStream, cursor)) {
okPublic += 1;
- } catch (PgpGeneralException e) {
- log.add(LogType.MSG_EXPORT_ERROR_KEY, 2);
- updateProgress(progress++, numKeys);
- continue;
- } finally {
- // make sure this is closed
- if (arOutStream != null) {
- arOutStream.close();
- }
- arOutStream = null;
- }
- if (exportSecret && cursor.getInt(3) > 0) {
- try {
- arOutStream = new ArmoredOutputStream(outStream);
-
- // export secret key part
- log.add(LogType.MSG_EXPORT_SECRET, 2, KeyFormattingUtils.beautifyKeyId
- (keyId));
- byte[] data = cursor.getBlob(2);
- CanonicalizedKeyRing ring =
- UncachedKeyRing.decodeFromData(data).canonicalize(log, 2, true);
- ring.encode(arOutStream);
-
- okSecret += 1;
- } catch (PgpGeneralException e) {
- log.add(LogType.MSG_EXPORT_ERROR_KEY, 2);
- updateProgress(progress++, numKeys);
- continue;
- } finally {
- // make sure this is closed
- if (arOutStream != null) {
- arOutStream.close();
+ boolean hasSecret = cursor.getInt(INDEX_HAS_ANY_SECRET) > 0;
+ if (exportSecret && hasSecret) {
+ log.add(LogType.MSG_EXPORT_SECRET, 2, KeyFormattingUtils.beautifyKeyId(keyId));
+ if (writeSecretKeyToStream(log, outStream, cursor)) {
+ okSecret += 1;
}
}
}
updateProgress(progress++, numKeys);
-
cursor.moveToNext();
}
@@ -317,7 +225,7 @@ public class ExportOperation extends BaseOperation<ExportKeyringParcel> {
} catch (IOException e) {
log.add(LogType.MSG_EXPORT_ERROR_IO, 1);
- return new ExportResult(ExportResult.RESULT_ERROR, log, okPublic, okSecret);
+ return false; // new ExportResult(ExportResult.RESULT_ERROR, log);
} finally {
// Make sure the stream is closed
if (outStream != null) try {
@@ -325,61 +233,80 @@ public class ExportOperation extends BaseOperation<ExportKeyringParcel> {
} catch (Exception e) {
Log.e(Constants.TAG, "error closing stream", e);
}
- if (cursor != null) {
- cursor.close();
- }
+ cursor.close();
}
-
- log.add(LogType.MSG_EXPORT_SUCCESS, 1);
- return new ExportResult(ExportResult.RESULT_OK, log, okPublic, okSecret);
+ return true;
}
- @NonNull
- public ExportResult execute(ExportKeyringParcel exportInput, CryptoInputParcel cryptoInput) {
- switch (exportInput.mExportType) {
- case UPLOAD_KEYSERVER: {
- Proxy proxy;
- if (cryptoInput.getParcelableProxy() == null) {
- // explicit proxy not set
- if (!OrbotHelper.isOrbotInRequiredState(mContext)) {
- return new ExportResult(null,
- RequiredInputParcel.createOrbotRequiredOperation(), cryptoInput);
- }
- proxy = Preferences.getPreferences(mContext).getProxyPrefs()
- .parcelableProxy.getProxy();
- } else {
- proxy = cryptoInput.getParcelableProxy().getProxy();
- }
+ private boolean writePublicKeyToStream(OperationLog log, OutputStream outStream, Cursor cursor)
+ throws IOException {
- HkpKeyserver hkpKeyserver = new HkpKeyserver(exportInput.mKeyserver);
- try {
- if (exportInput.mCanonicalizedPublicKeyringUri != null) {
- CanonicalizedPublicKeyRing keyring
- = mProviderHelper.getCanonicalizedPublicKeyRing(
- exportInput.mCanonicalizedPublicKeyringUri);
- return uploadKeyRingToServer(hkpKeyserver, keyring, proxy);
- } else {
- return uploadKeyRingToServer(hkpKeyserver, exportInput.mUncachedKeyRing,
- proxy);
- }
- } catch (ProviderHelper.NotFoundException e) {
- Log.e(Constants.TAG, "error uploading key", e);
- return new ExportResult(ExportResult.RESULT_ERROR, new OperationLog());
- }
- }
- case EXPORT_FILE: {
- return exportToFile(exportInput.mMasterKeyIds, exportInput.mExportSecret,
- exportInput.mOutputFile);
+ ArmoredOutputStream arOutStream = null;
+
+ try {
+ arOutStream = new ArmoredOutputStream(outStream);
+ byte[] data = cursor.getBlob(INDEX_PUBKEY_DATA);
+ CanonicalizedKeyRing ring = UncachedKeyRing.decodeFromData(data).canonicalize(log, 2, true);
+ ring.encode(arOutStream);
+
+ } catch (PgpGeneralException e) {
+ log.add(LogType.MSG_EXPORT_ERROR_KEY, 2);
+ } finally {
+ if (arOutStream != null) {
+ arOutStream.close();
}
- case EXPORT_URI: {
- return exportToUri(exportInput.mMasterKeyIds, exportInput.mExportSecret,
- exportInput.mOutputUri);
+ }
+ return true;
+ }
+
+ private boolean writeSecretKeyToStream(OperationLog log, OutputStream outStream, Cursor cursor)
+ throws IOException {
+
+ ArmoredOutputStream arOutStream = null;
+
+ try {
+ arOutStream = new ArmoredOutputStream(outStream);
+ byte[] data = cursor.getBlob(INDEX_SECKEY_DATA);
+ CanonicalizedKeyRing ring = UncachedKeyRing.decodeFromData(data).canonicalize(log, 2, true);
+ ring.encode(arOutStream);
+
+ } catch (PgpGeneralException e) {
+ log.add(LogType.MSG_EXPORT_ERROR_KEY, 2);
+ } finally {
+ if (arOutStream != null) {
+ arOutStream.close();
}
- default: { // can never happen, all enum types must be handled above
- throw new AssertionError("must not happen, this is a bug!");
+ }
+ return true;
+ }
+
+ private Cursor queryForKeys(long[] masterKeyIds) {
+
+ String selection = null, selectionArgs[] = null;
+
+ if (masterKeyIds != null) {
+ // convert long[] to String[]
+ selectionArgs = new String[masterKeyIds.length];
+ for (int i = 0; i < masterKeyIds.length; i++) {
+ selectionArgs[i] = Long.toString(masterKeyIds[i]);
}
+
+ // generates ?,?,? as placeholders for selectionArgs
+ String placeholders = TextUtils.join(",",
+ Collections.nCopies(masterKeyIds.length, "?"));
+
+ // put together selection string
+ selection = Tables.KEYS + "." + KeyRings.MASTER_KEY_ID
+ + " IN (" + placeholders + ")";
}
+
+ return mProviderHelper.getContentResolver().query(
+ KeyRings.buildUnifiedKeyRingsUri(), PROJECTION, selection, selectionArgs,
+ Tables.KEYS + "." + KeyRings.MASTER_KEY_ID
+ );
+
}
+
} \ No newline at end of file
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java
index 7ec33874f..c48ccc500 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java
@@ -135,7 +135,7 @@ public class InputDataOperation extends BaseOperation<InputDataParcel> {
|| contentType != null
&& !contentType.startsWith("multipart/")
&& !contentType.startsWith("text/")
- && !contentType.startsWith("application/")) {
+ && !"application/octet-stream".equals(contentType)) {
skipMimeParsing = true;
}
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/UploadOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/UploadOperation.java
new file mode 100644
index 000000000..499f592cf
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/UploadOperation.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010-2014 Thialfihar <thi@thialfihar.org>
+ * Copyright (C) 2015 Vincent Breitmoser <valodim@mugenguild.com>
+ *
+ * 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.operations;
+
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.Proxy;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+
+import org.spongycastle.bcpg.ArmoredOutputStream;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.keyimport.HkpKeyserver;
+import org.sufficientlysecure.keychain.keyimport.Keyserver.AddKeyException;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+import org.sufficientlysecure.keychain.operations.results.UploadResult;
+import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing;
+import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing;
+import org.sufficientlysecure.keychain.pgp.Progressable;
+import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
+import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.service.UploadKeyringParcel;
+import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
+import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.util.Preferences;
+import org.sufficientlysecure.keychain.util.orbot.OrbotHelper;
+
+
+/**
+ * An operation class which implements high level export operations.
+ * This class receives a source and/or destination of keys as input and performs
+ * all steps for this export.
+ *
+ * @see org.sufficientlysecure.keychain.ui.adapter.ImportKeysAdapter#getSelectedEntries()
+ * For the export operation, the input consists of a set of key ids and
+ * either the name of a file or an output uri to write to.
+ */
+public class UploadOperation extends BaseOperation<UploadKeyringParcel> {
+
+ public UploadOperation(Context context, ProviderHelper providerHelper, Progressable
+ progressable) {
+ super(context, providerHelper, progressable);
+ }
+
+ public UploadOperation(Context context, ProviderHelper providerHelper,
+ Progressable progressable, AtomicBoolean cancelled) {
+ super(context, providerHelper, progressable, cancelled);
+ }
+
+ @NonNull
+ public UploadResult execute(UploadKeyringParcel uploadInput, CryptoInputParcel cryptoInput) {
+ Proxy proxy;
+ if (cryptoInput.getParcelableProxy() == null) {
+ // explicit proxy not set
+ if (!OrbotHelper.isOrbotInRequiredState(mContext)) {
+ return new UploadResult(null, RequiredInputParcel.createOrbotRequiredOperation(), cryptoInput);
+ }
+ proxy = Preferences.getPreferences(mContext).getProxyPrefs().parcelableProxy.getProxy();
+ } else {
+ proxy = cryptoInput.getParcelableProxy().getProxy();
+ }
+
+ HkpKeyserver hkpKeyserver = new HkpKeyserver(uploadInput.mKeyserver);
+ try {
+ CanonicalizedPublicKeyRing keyring;
+ if (uploadInput.mMasterKeyId != null) {
+ keyring = mProviderHelper.getCanonicalizedPublicKeyRing(
+ uploadInput.mMasterKeyId);
+ } else if (uploadInput.mUncachedKeyringBytes != null) {
+ CanonicalizedKeyRing canonicalizedRing =
+ UncachedKeyRing.decodeFromData(uploadInput.mUncachedKeyringBytes)
+ .canonicalize(new OperationLog(), 0, true);
+ if ( ! CanonicalizedPublicKeyRing.class.isInstance(canonicalizedRing)) {
+ throw new AssertionError("keyring bytes must contain public key ring!");
+ }
+ keyring = (CanonicalizedPublicKeyRing) canonicalizedRing;
+ } else {
+ throw new AssertionError("key id or bytes must be non-null!");
+ }
+ return uploadKeyRingToServer(hkpKeyserver, keyring, proxy);
+ } catch (ProviderHelper.NotFoundException e) {
+ Log.e(Constants.TAG, "error uploading key", e);
+ return new UploadResult(UploadResult.RESULT_ERROR, new OperationLog());
+ } catch (IOException e) {
+ e.printStackTrace();
+ return new UploadResult(UploadResult.RESULT_ERROR, new OperationLog());
+ } catch (PgpGeneralException e) {
+ e.printStackTrace();
+ return new UploadResult(UploadResult.RESULT_ERROR, new OperationLog());
+ }
+ }
+
+ UploadResult uploadKeyRingToServer(HkpKeyserver server, CanonicalizedPublicKeyRing keyring, Proxy proxy) {
+
+ mProgressable.setProgress(R.string.progress_uploading, 0, 1);
+
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ ArmoredOutputStream aos = null;
+ OperationLog log = new OperationLog();
+ log.add(LogType.MSG_EXPORT_UPLOAD_PUBLIC, 0, KeyFormattingUtils.convertKeyIdToHex(
+ keyring.getPublicKey().getKeyId()
+ ));
+
+ try {
+ aos = new ArmoredOutputStream(bos);
+ keyring.encode(aos);
+ aos.close();
+
+ String armoredKey = bos.toString("UTF-8");
+ server.add(armoredKey, proxy);
+
+ log.add(LogType.MSG_EXPORT_UPLOAD_SUCCESS, 1);
+ return new UploadResult(UploadResult.RESULT_OK, log);
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "IOException", e);
+
+ log.add(LogType.MSG_EXPORT_ERROR_KEY, 1);
+ return new UploadResult(UploadResult.RESULT_ERROR, log);
+ } catch (AddKeyException e) {
+ Log.e(Constants.TAG, "AddKeyException", e);
+
+ log.add(LogType.MSG_EXPORT_ERROR_UPLOAD, 1);
+ return new UploadResult(UploadResult.RESULT_ERROR, log);
+ } finally {
+ mProgressable.setProgress(R.string.progress_uploading, 1, 1);
+ try {
+ if (aos != null) {
+ aos.close();
+ }
+ bos.close();
+ } catch (IOException e) {
+ // this is just a finally thing, no matter if it doesn't work out.
+ }
+ }
+ }
+
+} \ No newline at end of file
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ExportResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ExportResult.java
index e21ef949f..135f5af3d 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ExportResult.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ExportResult.java
@@ -24,39 +24,18 @@ import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
public class ExportResult extends InputPendingResult {
- final int mOkPublic, mOkSecret;
-
public ExportResult(int result, OperationLog log) {
- this(result, log, 0, 0);
- }
-
- public ExportResult(int result, OperationLog log, int okPublic, int okSecret) {
super(result, log);
- mOkPublic = okPublic;
- mOkSecret = okSecret;
- }
-
-
- public ExportResult(OperationLog log, RequiredInputParcel requiredInputParcel,
- CryptoInputParcel cryptoInputParcel) {
- super(log, requiredInputParcel, cryptoInputParcel);
- // we won't use these values
- mOkPublic = -1;
- mOkSecret = -1;
}
/** Construct from a parcel - trivial because we have no extra data. */
public ExportResult(Parcel source) {
super(source);
- mOkPublic = source.readInt();
- mOkSecret = source.readInt();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
- dest.writeInt(mOkPublic);
- dest.writeInt(mOkSecret);
}
public static Creator<ExportResult> CREATOR = new Creator<ExportResult>() {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java
index f959ddd76..24d1215d4 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java
@@ -765,16 +765,11 @@ public abstract class OperationResult implements Parcelable {
MSG_IMPORT_SUCCESS (LogLevel.OK, R.string.msg_import_success),
MSG_EXPORT (LogLevel.START, R.plurals.msg_export),
- MSG_EXPORT_FILE_NAME (LogLevel.INFO, R.string.msg_export_file_name),
MSG_EXPORT_UPLOAD_PUBLIC (LogLevel.START, R.string.msg_export_upload_public),
MSG_EXPORT_PUBLIC (LogLevel.DEBUG, R.string.msg_export_public),
MSG_EXPORT_SECRET (LogLevel.DEBUG, R.string.msg_export_secret),
MSG_EXPORT_ALL (LogLevel.START, R.string.msg_export_all),
- MSG_EXPORT_ERROR_NO_FILE (LogLevel.ERROR, R.string.msg_export_error_no_file),
- MSG_EXPORT_ERROR_FOPEN (LogLevel.ERROR, R.string.msg_export_error_fopen),
- MSG_EXPORT_ERROR_NO_URI (LogLevel.ERROR, R.string.msg_export_error_no_uri),
MSG_EXPORT_ERROR_URI_OPEN (LogLevel.ERROR, R.string.msg_export_error_uri_open),
- MSG_EXPORT_ERROR_STORAGE (LogLevel.ERROR, R.string.msg_export_error_storage),
MSG_EXPORT_ERROR_DB (LogLevel.ERROR, R.string.msg_export_error_db),
MSG_EXPORT_ERROR_IO (LogLevel.ERROR, R.string.msg_export_error_io),
MSG_EXPORT_ERROR_KEY (LogLevel.ERROR, R.string.msg_export_error_key),
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/UploadResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/UploadResult.java
new file mode 100644
index 000000000..a88072de3
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/UploadResult.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com>
+ *
+ * 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.operations.results;
+
+import android.os.Parcel;
+
+import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
+import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
+
+
+public class UploadResult extends InputPendingResult {
+
+ final int mOkPublic, mOkSecret;
+
+ public UploadResult(int result, OperationLog log) {
+ this(result, log, 0, 0);
+ }
+
+ public UploadResult(int result, OperationLog log, int okPublic, int okSecret) {
+ super(result, log);
+ mOkPublic = okPublic;
+ mOkSecret = okSecret;
+ }
+
+
+ public UploadResult(OperationLog log, RequiredInputParcel requiredInputParcel,
+ CryptoInputParcel cryptoInputParcel) {
+ super(log, requiredInputParcel, cryptoInputParcel);
+ // we won't use these values
+ mOkPublic = -1;
+ mOkSecret = -1;
+ }
+
+ /** Construct from a parcel - trivial because we have no extra data. */
+ public UploadResult(Parcel source) {
+ super(source);
+ mOkPublic = source.readInt();
+ mOkSecret = source.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(mOkPublic);
+ dest.writeInt(mOkSecret);
+ }
+
+ public static Creator<UploadResult> CREATOR = new Creator<UploadResult>() {
+ public UploadResult createFromParcel(final Parcel source) {
+ return new UploadResult(source);
+ }
+
+ public UploadResult[] newArray(final int size) {
+ return new UploadResult[size];
+ }
+ };
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java
index 0709d4f62..2d28f70e0 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java
@@ -346,14 +346,14 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
if (!"".equals(originalFilename)) {
log.add(LogType.MSG_DC_CLEAR_META_FILE, indent + 1, originalFilename);
}
- log.add(LogType.MSG_DC_CLEAR_META_MIME, indent + 1,
- mimeType);
log.add(LogType.MSG_DC_CLEAR_META_TIME, indent + 1,
new Date(literalData.getModificationTime().getTime()).toString());
// return here if we want to decrypt the metadata only
if (input.isDecryptMetadataOnly()) {
+ log.add(LogType.MSG_DC_CLEAR_META_MIME, indent + 1, mimeType);
+
// this operation skips the entire stream to find the data length!
Long originalSize = literalData.findDataLength();
@@ -387,6 +387,7 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
long wholeSize = 0; // TODO inputData.getSize() - inputData.getStreamPosition();
int length;
byte[] buffer = new byte[1 << 16];
+ byte[] firstBytes = new byte[48];
while ((length = dataIn.read(buffer)) > 0) {
// Log.d(Constants.TAG, "read bytes: " + length);
if (out != null) {
@@ -396,6 +397,11 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
// update signature buffer if signature is also present
signatureChecker.updateSignatureData(buffer, 0, length);
+ // note down first couple of bytes for "magic bytes" file type detection
+ if (alreadyWritten == 0) {
+ System.arraycopy(buffer, 0, firstBytes, 0, length > firstBytes.length ? firstBytes.length : length);
+ }
+
alreadyWritten += length;
// noinspection ConstantConditions, TODO progress
if (wholeSize > 0) {
@@ -408,6 +414,17 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
}
}
+ // special treatment to detect pgp mime types
+ if (matchesPrefix(firstBytes, "-----BEGIN PGP PUBLIC KEY BLOCK-----")
+ || matchesPrefix(firstBytes, "-----BEGIN PGP PRIVATE KEY BLOCK-----")) {
+ mimeType = "application/pgp-keys";
+ } else if (matchesPrefix(firstBytes, "-----BEGIN PGP MESSAGE-----")) {
+ // this is NOT pgp/encrypted, see RFC 3156!
+ mimeType = "application/pgp-message";
+ }
+
+ log.add(LogType.MSG_DC_CLEAR_META_MIME, indent + 1, mimeType);
+
metadata = new OpenPgpMetadata(
originalFilename, mimeType, literalData.getModificationTime().getTime(), alreadyWritten, charset);
@@ -940,4 +957,16 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
return nl.getBytes();
}
+ /// Convenience method - Trivially checks if a byte array matches the bytes of a plain text string
+ // Assumes data.length >= needle.length()
+ static boolean matchesPrefix(byte[] data, String needle) {
+ byte[] needleBytes = needle.getBytes();
+ for (int i = 0; i < needle.length(); i++) {
+ if (data[i] != needleBytes[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java
index 406c64ae8..ecc190d97 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java
@@ -53,6 +53,7 @@ import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.Passphrase;
import org.sufficientlysecure.keychain.util.ProgressScaler;
+import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
@@ -361,7 +362,7 @@ public class PgpSignEncryptOperation extends BaseOperation {
long alreadyWritten = 0;
int length;
byte[] buffer = new byte[1 << 16];
- InputStream in = inputData.getInputStream();
+ InputStream in = new BufferedInputStream(inputData.getInputStream());
while ((length = in.read(buffer)) > 0) {
pOut.write(buffer, 0, length);
@@ -389,7 +390,7 @@ public class PgpSignEncryptOperation extends BaseOperation {
// write -----BEGIN PGP SIGNED MESSAGE-----
armorOut.beginClearText(input.getSignatureHashAlgorithm());
- InputStream in = inputData.getInputStream();
+ InputStream in = new BufferedInputStream(inputData.getInputStream());
final BufferedReader reader = new BufferedReader(new InputStreamReader(in));
// update signature buffer with first line
@@ -421,7 +422,7 @@ public class PgpSignEncryptOperation extends BaseOperation {
updateProgress(R.string.progress_signing, 8, 100);
log.add(LogType.MSG_PSE_SIGNING_DETACHED, indent);
- InputStream in = inputData.getInputStream();
+ InputStream in = new BufferedInputStream(inputData.getInputStream());
// handle output stream separately for detached signatures
detachedByteOut = new ByteArrayOutputStream();
@@ -458,7 +459,7 @@ public class PgpSignEncryptOperation extends BaseOperation {
updateProgress(R.string.progress_signing, 8, 100);
log.add(LogType.MSG_PSE_SIGNING, indent);
- InputStream in = inputData.getInputStream();
+ InputStream in = new BufferedInputStream(inputData.getInputStream());
if (enableCompression) {
compressGen = new PGPCompressedDataGenerator(input.getCompressionAlgorithm());
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java
index 96778649c..c967a5abc 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java
@@ -18,6 +18,23 @@
package org.sufficientlysecure.keychain.pgp;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.TreeSet;
+
import org.spongycastle.bcpg.ArmoredOutputStream;
import org.spongycastle.bcpg.PublicKeyAlgorithmTags;
import org.spongycastle.bcpg.SignatureSubpacketTags;
@@ -43,23 +60,6 @@ import org.sufficientlysecure.keychain.util.IterableIterator;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.Utf8Util;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Calendar;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.Set;
-import java.util.TimeZone;
-import java.util.TreeSet;
-
/** Wrapper around PGPKeyRing class, to be constructed from bytes.
*
* This class and its relatives UncachedPublicKey and UncachedSecretKey are
@@ -78,8 +78,7 @@ import java.util.TreeSet;
* @see org.sufficientlysecure.keychain.pgp.UncachedSecretKey
*
*/
-@SuppressWarnings("unchecked")
-public class UncachedKeyRing implements Serializable {
+public class UncachedKeyRing {
final PGPKeyRing mRing;
final boolean mIsSecret;
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ExportKeyringParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ExportKeyringParcel.java
index 24c002bbd..9b0e5573e 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ExportKeyringParcel.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ExportKeyringParcel.java
@@ -24,60 +24,31 @@ import android.os.Parcel;
import android.os.Parcelable;
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
+import org.sufficientlysecure.keychain.util.Passphrase;
+
public class ExportKeyringParcel implements Parcelable {
- public String mKeyserver;
public Uri mCanonicalizedPublicKeyringUri;
- public UncachedKeyRing mUncachedKeyRing;
+ public Passphrase mSymmetricPassphrase;
public boolean mExportSecret;
public long mMasterKeyIds[];
- public String mOutputFile;
public Uri mOutputUri;
- public ExportType mExportType;
-
- public enum ExportType {
- UPLOAD_KEYSERVER,
- EXPORT_FILE,
- EXPORT_URI
- }
-
- public ExportKeyringParcel(String keyserver, Uri keyringUri) {
- mExportType = ExportType.UPLOAD_KEYSERVER;
- mKeyserver = keyserver;
- mCanonicalizedPublicKeyringUri = keyringUri;
- }
-
- public ExportKeyringParcel(String keyserver, UncachedKeyRing uncachedKeyRing) {
- mExportType = ExportType.UPLOAD_KEYSERVER;
- mKeyserver = keyserver;
- mUncachedKeyRing = uncachedKeyRing;
- }
-
- public ExportKeyringParcel(long[] masterKeyIds, boolean exportSecret, String outputFile) {
- mExportType = ExportType.EXPORT_FILE;
- mMasterKeyIds = masterKeyIds;
- mExportSecret = exportSecret;
- mOutputFile = outputFile;
- }
- @SuppressWarnings("unused") // TODO: is it used?
- public ExportKeyringParcel(long[] masterKeyIds, boolean exportSecret, Uri outputUri) {
- mExportType = ExportType.EXPORT_URI;
+ public ExportKeyringParcel(Passphrase symmetricPassphrase,
+ long[] masterKeyIds, boolean exportSecret, Uri outputUri) {
+ mSymmetricPassphrase = symmetricPassphrase;
mMasterKeyIds = masterKeyIds;
mExportSecret = exportSecret;
mOutputUri = outputUri;
}
protected ExportKeyringParcel(Parcel in) {
- mKeyserver = in.readString();
mCanonicalizedPublicKeyringUri = (Uri) in.readValue(Uri.class.getClassLoader());
- mUncachedKeyRing = (UncachedKeyRing) in.readValue(UncachedKeyRing.class.getClassLoader());
mExportSecret = in.readByte() != 0x00;
- mOutputFile = in.readString();
mOutputUri = (Uri) in.readValue(Uri.class.getClassLoader());
- mExportType = (ExportType) in.readValue(ExportType.class.getClassLoader());
mMasterKeyIds = in.createLongArray();
+ mSymmetricPassphrase = in.readParcelable(getClass().getClassLoader());
}
@Override
@@ -87,14 +58,11 @@ public class ExportKeyringParcel implements Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
- dest.writeString(mKeyserver);
dest.writeValue(mCanonicalizedPublicKeyringUri);
- dest.writeValue(mUncachedKeyRing);
dest.writeByte((byte) (mExportSecret ? 0x01 : 0x00));
- dest.writeString(mOutputFile);
dest.writeValue(mOutputUri);
- dest.writeValue(mExportType);
dest.writeLongArray(mMasterKeyIds);
+ dest.writeParcelable(mSymmetricPassphrase, 0);
}
public static final Parcelable.Creator<ExportKeyringParcel> CREATOR = new Parcelable.Creator<ExportKeyringParcel>() {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java
index c7ac92eef..981a76203 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java
@@ -40,6 +40,7 @@ import org.sufficientlysecure.keychain.operations.InputDataOperation;
import org.sufficientlysecure.keychain.operations.PromoteKeyOperation;
import org.sufficientlysecure.keychain.operations.RevokeOperation;
import org.sufficientlysecure.keychain.operations.SignEncryptOperation;
+import org.sufficientlysecure.keychain.operations.UploadOperation;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation;
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel;
@@ -126,6 +127,8 @@ public class KeychainService extends Service implements Progressable {
op = new ImportOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled);
} else if (inputParcel instanceof ExportKeyringParcel) {
op = new ExportOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled);
+ } else if (inputParcel instanceof UploadKeyringParcel) {
+ op = new UploadOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled);
} else if (inputParcel instanceof ConsolidateInputParcel) {
op = new ConsolidateOperation(outerThis, new ProviderHelper(outerThis), outerThis);
} else if (inputParcel instanceof KeybaseVerificationParcel) {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/UploadKeyringParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/UploadKeyringParcel.java
new file mode 100644
index 000000000..0a14f3dc6
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/UploadKeyringParcel.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com>
+ * Copyright (C) 2015 Adithya Abraham Philip <adithyaphilip@gmail.com>
+ *
+ * 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.service;
+
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+
+public class UploadKeyringParcel implements Parcelable {
+ public String mKeyserver;
+
+ public final Long mMasterKeyId;
+ public final byte[] mUncachedKeyringBytes;
+
+ public UploadKeyringParcel(String keyserver, long masterKeyId) {
+ mKeyserver = keyserver;
+ mMasterKeyId = masterKeyId;
+ mUncachedKeyringBytes = null;
+ }
+
+ public UploadKeyringParcel(String keyserver, byte[] uncachedKeyringBytes) {
+ mKeyserver = keyserver;
+ mMasterKeyId = null;
+ mUncachedKeyringBytes = uncachedKeyringBytes;
+ }
+
+ protected UploadKeyringParcel(Parcel in) {
+ mKeyserver = in.readString();
+ mMasterKeyId = in.readInt() != 0 ? in.readLong() : null;
+ mUncachedKeyringBytes = in.createByteArray();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mKeyserver);
+ if (mMasterKeyId != null) {
+ dest.writeInt(1);
+ dest.writeLong(mMasterKeyId);
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeByteArray(mUncachedKeyringBytes);
+ }
+
+ public static final Creator<UploadKeyringParcel> CREATOR = new Creator<UploadKeyringParcel>() {
+ @Override
+ public UploadKeyringParcel createFromParcel(Parcel in) {
+ return new UploadKeyringParcel(in);
+ }
+
+ @Override
+ public UploadKeyringParcel[] newArray(int size) {
+ return new UploadKeyringParcel[size];
+ }
+ };
+} \ No newline at end of file
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupActivity.java
new file mode 100644
index 000000000..ff120c9b5
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupActivity.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com>
+ *
+ * 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.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.view.MenuItem;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.ui.base.BaseActivity;
+
+
+public class BackupActivity extends BaseActivity {
+
+ public static final String EXTRA_MASTER_KEY_IDS = "master_key_ids";
+ public static final String EXTRA_SECRET = "export_secret";
+
+ @Override
+ protected void initLayout() {
+ setContentView(R.layout.backup_activity);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // noinspection ConstantConditions, we know this activity has an action bar
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ if (savedInstanceState == null) {
+ Intent intent = getIntent();
+ boolean exportSecret = intent.getBooleanExtra(EXTRA_SECRET, false);
+ long[] masterKeyIds = intent.getLongArrayExtra(EXTRA_MASTER_KEY_IDS);
+
+ Fragment frag = BackupCodeFragment.newInstance(masterKeyIds, exportSecret);
+
+ FragmentManager fragMan = getSupportFragmentManager();
+ fragMan.beginTransaction()
+ .setCustomAnimations(0, 0)
+ .replace(R.id.content_frame, frag)
+ .commit();
+ }
+
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ FragmentManager fragMan = getSupportFragmentManager();
+ // pop from back stack, or if nothing was on there finish activity
+ if ( ! fragMan.popBackStackImmediate()) {
+ finish();
+ }
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeFragment.java
new file mode 100644
index 000000000..be1399a30
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeFragment.java
@@ -0,0 +1,550 @@
+/*
+ * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com>
+ *
+ * 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 java.io.File;
+import java.io.IOException;
+import java.security.SecureRandom;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Random;
+
+import android.animation.ArgbEvaluator;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.ColorInt;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentManager.OnBackStackChangedListener;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateInterpolator;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.operations.results.ExportResult;
+import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider;
+import org.sufficientlysecure.keychain.service.ExportKeyringParcel;
+import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment;
+import org.sufficientlysecure.keychain.ui.util.Notify;
+import org.sufficientlysecure.keychain.ui.util.Notify.ActionListener;
+import org.sufficientlysecure.keychain.ui.util.Notify.Style;
+import org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator;
+import org.sufficientlysecure.keychain.util.FileHelper;
+import org.sufficientlysecure.keychain.util.Passphrase;
+
+
+public class BackupCodeFragment extends CryptoOperationFragment<ExportKeyringParcel,ExportResult>
+ implements OnBackStackChangedListener {
+
+ public static final String ARG_BACKUP_CODE = "backup_code";
+ public static final String BACK_STACK_INPUT = "state_display";
+ public static final String ARG_EXPORT_SECRET = "export_secret";
+ public static final String ARG_MASTER_KEY_IDS = "master_key_ids";
+
+ public static final String ARG_CURRENT_STATE = "current_state";
+
+ public static final int REQUEST_SAVE = 1;
+ public static final String ARG_BACK_STACK = "back_stack";
+
+ // argument variables
+ private boolean mExportSecret;
+ private long[] mMasterKeyIds;
+ String mBackupCode;
+
+ private EditText[] mCodeEditText;
+ private ToolableViewAnimator mStatusAnimator, mTitleAnimator, mCodeFieldsAnimator;
+
+ private Integer mBackStackLevel;
+
+ private Uri mCachedExportUri;
+ private boolean mShareNotSave;
+
+ public static BackupCodeFragment newInstance(long[] masterKeyIds, boolean exportSecret) {
+ BackupCodeFragment frag = new BackupCodeFragment();
+
+ Bundle args = new Bundle();
+ args.putString(ARG_BACKUP_CODE, generateRandomCode());
+ args.putLongArray(ARG_MASTER_KEY_IDS, masterKeyIds);
+ args.putBoolean(ARG_EXPORT_SECRET, exportSecret);
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ enum BackupCodeState {
+ STATE_UNINITIALIZED, STATE_DISPLAY, STATE_INPUT, STATE_INPUT_ERROR, STATE_OK
+ }
+
+ BackupCodeState mCurrentState = BackupCodeState.STATE_UNINITIALIZED;
+
+ void switchState(BackupCodeState state, boolean animate) {
+
+ switch (state) {
+ case STATE_UNINITIALIZED:
+ throw new AssertionError("can't switch to uninitialized state, this is a bug!");
+
+ case STATE_DISPLAY:
+ mTitleAnimator.setDisplayedChild(0, animate);
+ mStatusAnimator.setDisplayedChild(0, animate);
+ mCodeFieldsAnimator.setDisplayedChild(0, animate);
+
+ break;
+
+ case STATE_INPUT:
+ mTitleAnimator.setDisplayedChild(1, animate);
+ mStatusAnimator.setDisplayedChild(1, animate);
+ mCodeFieldsAnimator.setDisplayedChild(1, animate);
+
+ for (EditText editText : mCodeEditText) {
+ editText.setText("");
+ }
+
+ pushBackStackEntry();
+
+ break;
+
+ case STATE_INPUT_ERROR: {
+ mTitleAnimator.setDisplayedChild(1, false);
+ mStatusAnimator.setDisplayedChild(2, animate);
+ mCodeFieldsAnimator.setDisplayedChild(1, false);
+
+ hideKeyboard();
+
+ if (animate) {
+ @ColorInt int black = mCodeEditText[0].getCurrentTextColor();
+ @ColorInt int red = getResources().getColor(R.color.android_red_dark);
+ animateFlashText(mCodeEditText, black, red, false);
+ }
+
+ break;
+ }
+
+ case STATE_OK: {
+ mTitleAnimator.setDisplayedChild(2, animate);
+ mStatusAnimator.setDisplayedChild(3, animate);
+ mCodeFieldsAnimator.setDisplayedChild(1, false);
+
+ hideKeyboard();
+
+ for (EditText editText : mCodeEditText) {
+ editText.setEnabled(false);
+ }
+
+ @ColorInt int green = getResources().getColor(R.color.android_green_dark);
+ if (animate) {
+ @ColorInt int black = mCodeEditText[0].getCurrentTextColor();
+ animateFlashText(mCodeEditText, black, green, true);
+ } else {
+ for (TextView textView : mCodeEditText) {
+ textView.setTextColor(green);
+ }
+ }
+
+ popBackStackNoAction();
+
+ break;
+ }
+
+ }
+
+ mCurrentState = state;
+
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.backup_code_fragment, container, false);
+
+ Bundle args = getArguments();
+ mBackupCode = args.getString(ARG_BACKUP_CODE);
+ mMasterKeyIds = args.getLongArray(ARG_MASTER_KEY_IDS);
+ mExportSecret = args.getBoolean(ARG_EXPORT_SECRET);
+
+ mCodeEditText = new EditText[4];
+ mCodeEditText[0] = (EditText) view.findViewById(R.id.backup_code_1);
+ mCodeEditText[1] = (EditText) view.findViewById(R.id.backup_code_2);
+ mCodeEditText[2] = (EditText) view.findViewById(R.id.backup_code_3);
+ mCodeEditText[3] = (EditText) view.findViewById(R.id.backup_code_4);
+
+ {
+ TextView[] codeDisplayText = new TextView[4];
+ codeDisplayText[0] = (TextView) view.findViewById(R.id.backup_code_display_1);
+ codeDisplayText[1] = (TextView) view.findViewById(R.id.backup_code_display_2);
+ codeDisplayText[2] = (TextView) view.findViewById(R.id.backup_code_display_3);
+ codeDisplayText[3] = (TextView) view.findViewById(R.id.backup_code_display_4);
+
+ // set backup code in code TextViews
+ char[] backupCode = mBackupCode.toCharArray();
+ for (int i = 0; i < codeDisplayText.length; i++) {
+ codeDisplayText[i].setText(backupCode, i * 7, 6);
+ }
+
+ // set background to null in TextViews - this will retain padding from EditText style!
+ for (TextView textView : codeDisplayText) {
+ // noinspection deprecation, setBackground(Drawable) is API level >=16
+ textView.setBackgroundDrawable(null);
+ }
+ }
+
+ setupEditTextFocusNext(mCodeEditText);
+ setupEditTextSuccessListener(mCodeEditText);
+
+ mStatusAnimator = (ToolableViewAnimator) view.findViewById(R.id.status_animator);
+ mTitleAnimator = (ToolableViewAnimator) view.findViewById(R.id.title_animator);
+ mCodeFieldsAnimator = (ToolableViewAnimator) view.findViewById(R.id.code_animator);
+
+ View backupInput = view.findViewById(R.id.button_backup_input);
+ backupInput.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ switchState(BackupCodeState.STATE_INPUT, true);
+ }
+ });
+
+ view.findViewById(R.id.button_backup_save).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mShareNotSave = false;
+ startBackup();
+ }
+ });
+
+ view.findViewById(R.id.button_backup_share).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mShareNotSave = true;
+ startBackup();
+ }
+ });
+
+ view.findViewById(R.id.button_backup_back).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ FragmentManager fragMan = getFragmentManager();
+ if (fragMan != null) {
+ fragMan.popBackStack();
+ }
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ if (savedInstanceState != null) {
+ int savedBackStack = savedInstanceState.getInt(ARG_BACK_STACK);
+ if (savedBackStack >= 0) {
+ mBackStackLevel = savedBackStack;
+ // unchecked use, we know that this one is available in onViewCreated
+ getFragmentManager().addOnBackStackChangedListener(this);
+ }
+ BackupCodeState savedState = BackupCodeState.values()[savedInstanceState.getInt(ARG_CURRENT_STATE)];
+ switchState(savedState, false);
+ } else if (mCurrentState == BackupCodeState.STATE_UNINITIALIZED) {
+ switchState(BackupCodeState.STATE_DISPLAY, true);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(ARG_CURRENT_STATE, mCurrentState.ordinal());
+ outState.putInt(ARG_BACK_STACK, mBackStackLevel == null ? -1 : mBackStackLevel);
+ }
+
+ private void setupEditTextSuccessListener(final EditText[] backupCodes) {
+ for (EditText backupCode : backupCodes) {
+
+ backupCode.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) {
+ if (s.length() > 6) {
+ throw new AssertionError("max length of each field is 6!");
+ }
+
+ boolean inInputState = mCurrentState == BackupCodeState.STATE_INPUT
+ || mCurrentState == BackupCodeState.STATE_INPUT_ERROR;
+ boolean partIsComplete = s.length() == 6;
+ if (!inInputState || !partIsComplete) {
+ return;
+ }
+
+ checkIfCodeIsCorrect();
+ }
+ });
+
+ }
+ }
+
+ private void checkIfCodeIsCorrect() {
+
+ StringBuilder backupCodeInput = new StringBuilder(26);
+ for (EditText editText : mCodeEditText) {
+ if (editText.getText().length() < 6) {
+ return;
+ }
+ backupCodeInput.append(editText.getText());
+ backupCodeInput.append('-');
+ }
+ backupCodeInput.deleteCharAt(backupCodeInput.length() - 1);
+
+ // if they don't match, do nothing
+ if (backupCodeInput.toString().equals(mBackupCode)) {
+ switchState(BackupCodeState.STATE_OK, true);
+ return;
+ }
+
+ // TODO remove debug code
+ if (backupCodeInput.toString().startsWith("ABC")) {
+ switchState(BackupCodeState.STATE_OK, true);
+ return;
+ }
+
+ switchState(BackupCodeState.STATE_INPUT_ERROR, true);
+
+ }
+
+ private static void animateFlashText(
+ final TextView[] textViews, int color1, int color2, boolean staySecondColor) {
+
+ ValueAnimator anim = ValueAnimator.ofObject(new ArgbEvaluator(), color1, color2);
+ anim.addUpdateListener(new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animator) {
+ for (TextView textView : textViews) {
+ textView.setTextColor((Integer) animator.getAnimatedValue());
+ }
+ }
+ });
+ anim.setRepeatMode(ValueAnimator.REVERSE);
+ anim.setRepeatCount(staySecondColor ? 4 : 5);
+ anim.setDuration(180);
+ anim.setInterpolator(new AccelerateInterpolator());
+ anim.start();
+
+ }
+
+ private static void setupEditTextFocusNext(final EditText[] backupCodes) {
+ for (int i = 0; i < backupCodes.length -1; i++) {
+
+ final int next = i+1;
+
+ backupCodes[i].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) {
+ boolean inserting = before < count;
+ boolean cursorAtEnd = (start + count) == 6;
+
+ if (inserting && cursorAtEnd) {
+ backupCodes[next].requestFocus();
+ }
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ });
+
+ }
+ }
+
+ private void pushBackStackEntry() {
+ if (mBackStackLevel != null) {
+ return;
+ }
+ FragmentManager fragMan = getFragmentManager();
+ mBackStackLevel = fragMan.getBackStackEntryCount();
+ fragMan.beginTransaction().addToBackStack(BACK_STACK_INPUT).commit();
+ fragMan.addOnBackStackChangedListener(this);
+ }
+
+ private void popBackStackNoAction() {
+ FragmentManager fragMan = getFragmentManager();
+ fragMan.removeOnBackStackChangedListener(this);
+ fragMan.popBackStackImmediate(BACK_STACK_INPUT, FragmentManager.POP_BACK_STACK_INCLUSIVE);
+ mBackStackLevel = null;
+ }
+
+ @Override
+ public void onBackStackChanged() {
+ FragmentManager fragMan = getFragmentManager();
+ if (mBackStackLevel != null && fragMan.getBackStackEntryCount() == mBackStackLevel) {
+ fragMan.removeOnBackStackChangedListener(this);
+ switchState(BackupCodeState.STATE_DISPLAY, true);
+ mBackStackLevel = null;
+ }
+ }
+
+ private void startBackup() {
+
+ FragmentActivity activity = getActivity();
+ if (activity == null) {
+ return;
+ }
+
+ if (mCachedExportUri == null) {
+ mCachedExportUri = TemporaryStorageProvider.createFile(activity);
+ cryptoOperation();
+ return;
+ }
+
+ if (mShareNotSave) {
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setType("application/octet-stream");
+ intent.putExtra(Intent.EXTRA_STREAM, mCachedExportUri);
+ startActivity(intent);
+ } else {
+ saveFile(false);
+ }
+
+ }
+
+ private void saveFile(boolean overwrite) {
+ FragmentActivity activity = getActivity();
+ if (activity == null) {
+ return;
+ }
+
+ String date = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date());
+
+ // for kitkat and above, we have the document api
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ String filename = "openkeychain_backup_" + date + (mExportSecret ? ".gpg" : ".pub.gpg");
+ FileHelper.saveDocument(this, "application/octet-stream", filename, REQUEST_SAVE);
+ return;
+ }
+
+ File file = new File(Constants.Path.APP_DIR, "backup_" + date + (mExportSecret ? ".gpg" : ".pub.gpg"));
+
+ if (!overwrite && file.exists()) {
+ Notify.create(activity, R.string.snack_backup_exists, Style.WARN, new ActionListener() {
+ @Override
+ public void onAction() {
+ saveFile(true);
+ }
+ }, R.string.snack_btn_overwrite).show();
+ return;
+ }
+
+ try {
+ FileHelper.copyUriData(activity, mCachedExportUri, Uri.fromFile(file));
+ Notify.create(activity, R.string.snack_backup_saved_dir, Style.OK).show();
+ } catch (IOException e) {
+ Notify.create(activity, R.string.snack_backup_error_saving, Style.ERROR).show();
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode != REQUEST_SAVE) {
+ super.onActivityResult(requestCode, resultCode, data);
+ return;
+ }
+
+ if (resultCode != FragmentActivity.RESULT_OK) {
+ return;
+ }
+
+ FragmentActivity activity = getActivity();
+ if (activity == null) {
+ return;
+ }
+ try {
+ Uri outputUri = data.getData();
+ FileHelper.copyUriData(activity, mCachedExportUri, outputUri);
+ Notify.create(activity, R.string.snack_backup_saved, Style.OK).show();
+ } catch (IOException e) {
+ Notify.create(activity, R.string.snack_backup_error_saving, Style.ERROR).show();
+ }
+ }
+
+ @Nullable
+ @Override
+ public ExportKeyringParcel createOperationInput() {
+ // TODO replace debug code with real thing
+ // return new ExportKeyringParcel(new Passphrase(mBackupCode), mMasterKeyIds, mExportSecret, mCachedExportUri);
+ return new ExportKeyringParcel(new Passphrase("abc"), mMasterKeyIds, mExportSecret, mCachedExportUri);
+ }
+
+ @Override
+ public void onCryptoOperationSuccess(ExportResult result) {
+ startBackup();
+ }
+
+ @Override
+ public void onCryptoOperationError(ExportResult result) {
+ result.createNotify(getActivity()).show();
+ mCachedExportUri = null;
+ }
+
+ @Override
+ public void onCryptoOperationCancelled() {
+ mCachedExportUri = null;
+ }
+
+ @NonNull
+ private static String generateRandomCode() {
+
+ Random r = new SecureRandom();
+
+ // simple generation of a 20 character backup code
+ StringBuilder code = new StringBuilder(28);
+ for (int i = 0; i < 24; i++) {
+ if (i == 6 || i == 12 || i == 18) {
+ code.append('-');
+ }
+ code.append((char) ('A' + r.nextInt(26)));
+ }
+
+ return code.toString();
+
+ }
+
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java
index 739eb3e35..b79e4454d 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java
@@ -37,17 +37,17 @@ import org.spongycastle.bcpg.sig.KeyFlags;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.EditKeyResult;
-import org.sufficientlysecure.keychain.operations.results.ExportResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
+import org.sufficientlysecure.keychain.operations.results.UploadResult;
import org.sufficientlysecure.keychain.pgp.KeyRing;
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
-import org.sufficientlysecure.keychain.service.ExportKeyringParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel;
+import org.sufficientlysecure.keychain.service.UploadKeyringParcel;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
@@ -69,7 +69,7 @@ public class CreateKeyFinalFragment extends Fragment {
SaveKeyringParcel mSaveKeyringParcel;
- private CryptoOperationHelper<ExportKeyringParcel, ExportResult> mUploadOpHelper;
+ private CryptoOperationHelper<UploadKeyringParcel, UploadResult> mUploadOpHelper;
private CryptoOperationHelper<SaveKeyringParcel, EditKeyResult> mCreateOpHelper;
private CryptoOperationHelper<SaveKeyringParcel, EditKeyResult> mMoveToCardOpHelper;
@@ -407,20 +407,20 @@ public class CreateKeyFinalFragment extends Fragment {
}
// set data uri as path to keyring
- final Uri blobUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(saveKeyResult.mMasterKeyId);
+ final long masterKeyId = saveKeyResult.mMasterKeyId;
// upload to favorite keyserver
final String keyserver = Preferences.getPreferences(activity).getPreferredKeyserver();
- CryptoOperationHelper.Callback<ExportKeyringParcel, ExportResult> callback
- = new CryptoOperationHelper.Callback<ExportKeyringParcel, ExportResult>() {
+ CryptoOperationHelper.Callback<UploadKeyringParcel, UploadResult> callback
+ = new CryptoOperationHelper.Callback<UploadKeyringParcel, UploadResult>() {
@Override
- public ExportKeyringParcel createOperationInput() {
- return new ExportKeyringParcel(keyserver, blobUri);
+ public UploadKeyringParcel createOperationInput() {
+ return new UploadKeyringParcel(keyserver, masterKeyId);
}
@Override
- public void onCryptoOperationSuccess(ExportResult result) {
+ public void onCryptoOperationSuccess(UploadResult result) {
handleResult(result);
}
@@ -430,11 +430,11 @@ public class CreateKeyFinalFragment extends Fragment {
}
@Override
- public void onCryptoOperationError(ExportResult result) {
+ public void onCryptoOperationError(UploadResult result) {
handleResult(result);
}
- public void handleResult(ExportResult result) {
+ public void handleResult(UploadResult result) {
saveKeyResult.getLog().add(result, 0);
finishWithResult(saveKeyResult);
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java
index a0650f8b1..13838e77c 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java
@@ -24,6 +24,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
+import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ClipDescription;
import android.content.Context;
@@ -36,6 +37,7 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
+import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v7.widget.DefaultItemAnimator;
@@ -249,6 +251,7 @@ public class DecryptListFragment
}
}
+ @TargetApi(VERSION_CODES.KITKAT)
private void saveFileDialog(InputDataResult result, int index) {
Activity activity = getActivity();
@@ -260,13 +263,13 @@ public class DecryptListFragment
mCurrentSaveFileUri = result.getOutputUris().get(index);
String filename = metadata.getFilename();
- if (filename == null) {
+ if (TextUtils.isEmpty(filename)) {
String ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(metadata.getMimeType());
filename = "decrypted" + (ext != null ? "."+ext : "");
}
- FileHelper.saveDocument(this, filename, metadata.getMimeType(),
- REQUEST_CODE_OUTPUT);
+ // requires >=kitkat
+ FileHelper.saveDocument(this, metadata.getMimeType(), filename, REQUEST_CODE_OUTPUT);
}
private void saveFile(Uri saveUri) {
@@ -374,6 +377,9 @@ public class DecryptListFragment
if (ClipDescription.compareMimeTypes(type, "text/plain")) {
// noinspection deprecation, this should be called from Context, but not available in minSdk
icon = getResources().getDrawable(R.drawable.ic_chat_black_24dp);
+ } else if (ClipDescription.compareMimeTypes(type, "application/pgp-keys")) {
+ // noinspection deprecation, this should be called from Context, but not available in minSdk
+ icon = getResources().getDrawable(R.drawable.ic_key_plus_grey600_24dp);
} else if (ClipDescription.compareMimeTypes(type, "image/*")) {
int px = FormattingUtils.dpToPx(context, 32);
Bitmap bitmap = FileHelper.getThumbnail(context, outputUri, new Point(px, px));
@@ -764,11 +770,14 @@ public class DecryptListFragment
String filename;
if (metadata == null) {
filename = getString(R.string.filename_unknown);
- } else if (TextUtils.isEmpty(metadata.getFilename())) {
- filename = getString("text/plain".equals(metadata.getMimeType())
- ? R.string.filename_unknown_text : R.string.filename_unknown);
- } else {
+ } else if ( ! TextUtils.isEmpty(metadata.getFilename())) {
filename = metadata.getFilename();
+ } else if (ClipDescription.compareMimeTypes(metadata.getMimeType(), "application/pgp-keys")) {
+ filename = getString(R.string.filename_keys);
+ } else if (ClipDescription.compareMimeTypes(metadata.getMimeType(), "text/plain")) {
+ filename = getString(R.string.filename_unknown_text);
+ } else {
+ filename = getString(R.string.filename_unknown);
}
fileHolder.vFilename.setText(filename);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DrawerBackupFragment.java
index a3ea8ad9a..cf47dfc94 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DrawerBackupFragment.java
@@ -18,11 +18,7 @@
package org.sufficientlysecure.keychain.ui;
-import java.io.File;
-import java.text.SimpleDateFormat;
import java.util.ArrayList;
-import java.util.Date;
-import java.util.Locale;
import android.app.Activity;
import android.content.ContentResolver;
@@ -37,13 +33,11 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
-import org.sufficientlysecure.keychain.util.ExportHelper;
-public class BackupFragment extends Fragment {
+public class DrawerBackupFragment extends Fragment {
// This ids for multiple key export.
private ArrayList<Long> mIdsForRepeatAskPassphrase;
@@ -51,24 +45,10 @@ public class BackupFragment extends Fragment {
private int mIndex;
static final int REQUEST_REPEAT_PASSPHRASE = 1;
- private ExportHelper mExportHelper;
-
- @Override
- public void onAttach(Activity activity) {
- super.onAttach(activity);
- // we won't get attached to a non-fragment activity, so the cast should be safe
- mExportHelper = new ExportHelper((FragmentActivity) activity);
- }
-
- @Override
- public void onDetach() {
- super.onDetach();
- mExportHelper = null;
- }
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- View view = inflater.inflate(R.layout.backup_fragment, container, false);
+ View view = inflater.inflate(R.layout.drawer_backup_fragment, container, false);
View backupAll = view.findViewById(R.id.backup_all);
View backupPublicKeys = view.findViewById(R.id.backup_public_keys);
@@ -187,14 +167,11 @@ public class BackupFragment extends Fragment {
}
private void startBackup(boolean exportSecret) {
- File filename;
- String date = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date());
- if (exportSecret) {
- filename = new File(Constants.Path.APP_DIR, "keys_" + date + ".asc");
- } else {
- filename = new File(Constants.Path.APP_DIR, "keys_" + date + ".pub.asc");
- }
- mExportHelper.showExportKeysDialog(null, filename, exportSecret);
+
+ Intent intent = new Intent(getActivity(), BackupActivity.class);
+ intent.putExtra(BackupActivity.EXTRA_SECRET, exportSecret);
+ startActivity(intent);
+
}
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java
index 19603a549..bfe9ea290 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java
@@ -25,6 +25,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
@@ -34,6 +35,7 @@ import android.graphics.Bitmap;
import android.graphics.Point;
import android.net.Uri;
import android.os.Build;
+import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v7.widget.DefaultItemAnimator;
@@ -215,6 +217,7 @@ public class EncryptFilesFragment
mSelectedFiles.requestFocus();
}
+ @TargetApi(VERSION_CODES.KITKAT)
private void showOutputFileDialog() {
if (mFilesAdapter.getModelCount() != 1) {
throw new IllegalStateException();
@@ -223,8 +226,7 @@ public class EncryptFilesFragment
String targetName =
(mEncryptFilenames ? "1" : FileHelper.getFilename(getActivity(), model.inputUri))
+ (mUseArmor ? Constants.FILE_EXTENSION_ASC : Constants.FILE_EXTENSION_PGP_MAIN);
- FileHelper.saveDocument(this, targetName,
- REQUEST_CODE_OUTPUT);
+ FileHelper.saveDocument(this, targetName, REQUEST_CODE_OUTPUT);
}
public void addFile(Intent data) {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java
index c578bcf15..7f7532ddf 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java
@@ -21,8 +21,8 @@ import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
-import android.os.Message;
import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
@@ -33,10 +33,8 @@ import org.sufficientlysecure.keychain.intents.OpenKeychainIntents;
import org.sufficientlysecure.keychain.keyimport.ImportKeysListEntry;
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
-import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
import org.sufficientlysecure.keychain.ui.base.BaseNfcActivity;
-import org.sufficientlysecure.keychain.service.ServiceProgressHandler;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.ui.util.Notify;
@@ -78,10 +76,8 @@ public class ImportKeysActivity extends BaseNfcActivity
public static final String EXTRA_PENDING_INTENT_DATA = "data";
private Intent mPendingIntentData;
- // view
- private ImportKeysListFragment mListFragment;
- private Fragment mTopFragment;
- private View mImportButton;
+ public static final String TAG_FRAG_LIST = "frag_list";
+ public static final String TAG_FRAG_TOP = "frag_top";
// for CryptoOperationHelper.Callback
private String mKeyserver;
@@ -94,15 +90,22 @@ public class ImportKeysActivity extends BaseNfcActivity
super.onCreate(savedInstanceState);
setFullScreenDialogClose(Activity.RESULT_CANCELED, true);
- mImportButton = findViewById(R.id.import_import);
- mImportButton.setOnClickListener(new OnClickListener() {
+ findViewById(R.id.import_import).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
- importKeys();
+ importSelectedKeys();
}
});
- handleActions(savedInstanceState, getIntent());
+ // only used for OpenPgpService
+ if (getIntent().hasExtra(EXTRA_PENDING_INTENT_DATA)) {
+ mPendingIntentData = getIntent().getParcelableExtra(EXTRA_PENDING_INTENT_DATA);
+ }
+
+ // if we aren't being restored, initialize fragments
+ if (savedInstanceState == null) {
+ handleActions(getIntent());
+ }
}
@Override
@@ -110,7 +113,7 @@ public class ImportKeysActivity extends BaseNfcActivity
setContentView(R.layout.import_keys_activity);
}
- protected void handleActions(Bundle savedInstanceState, Intent intent) {
+ protected void handleActions(Intent intent) {
String action = intent.getAction();
Bundle extras = intent.getExtras();
Uri dataUri = intent.getData();
@@ -120,14 +123,8 @@ public class ImportKeysActivity extends BaseNfcActivity
extras = new Bundle();
}
- if (action == null) {
- startCloudFragment(savedInstanceState, null, false, null);
- startListFragment(savedInstanceState, null, null, null, null);
- return;
- }
-
if (Intent.ACTION_VIEW.equals(action)) {
- if (scheme.equals("http") || scheme.equals("https")) {
+ if ("http".equals(scheme) || "https".equals(scheme)) {
action = ACTION_SEARCH_KEYSERVER_FROM_URL;
} else {
// Android's Action when opening file associated to Keychain (see AndroidManifest.xml)
@@ -135,20 +132,24 @@ public class ImportKeysActivity extends BaseNfcActivity
action = ACTION_IMPORT_KEY;
}
}
+ if (action == null) {
+ // -> switch to default below
+ action = "";
+ }
switch (action) {
case ACTION_IMPORT_KEY: {
- /* Keychain's own Actions */
- startFileFragment(savedInstanceState);
-
if (dataUri != null) {
// action: directly load data
- startListFragment(savedInstanceState, null, dataUri, null, null);
+ startListFragment(null, dataUri, null, null);
} else if (extras.containsKey(EXTRA_KEY_BYTES)) {
byte[] importData = extras.getByteArray(EXTRA_KEY_BYTES);
// action: directly load data
- startListFragment(savedInstanceState, importData, null, null, null);
+ startListFragment(importData, null, null, null);
+ } else {
+ startTopFileFragment();
+ startListFragment(null, null, null, null);
}
break;
}
@@ -156,10 +157,6 @@ public class ImportKeysActivity extends BaseNfcActivity
case ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_TO_SERVICE:
case ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_RESULT: {
- // 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 */
@@ -175,10 +172,10 @@ public class ImportKeysActivity extends BaseNfcActivity
if (query != null && query.length() > 0) {
// display keyserver fragment with query
- startCloudFragment(savedInstanceState, query, false, null);
+ startTopCloudFragment(query, false, null);
// action: search immediately
- startListFragment(savedInstanceState, null, null, query, null);
+ startListFragment(null, null, query, null);
} else {
Log.e(Constants.TAG, "Query is empty!");
return;
@@ -194,10 +191,10 @@ public class ImportKeysActivity extends BaseNfcActivity
String query = "0x" + fingerprint;
// display keyserver fragment with query
- startCloudFragment(savedInstanceState, query, true, null);
+ startTopCloudFragment(query, true, null);
// action: search immediately
- startListFragment(savedInstanceState, null, null, query, null);
+ startListFragment(null, null, query, null);
}
} else {
Log.e(Constants.TAG,
@@ -208,14 +205,6 @@ public class ImportKeysActivity extends BaseNfcActivity
}
break;
}
- case ACTION_IMPORT_KEY_FROM_FILE: {
- // NOTE: this only displays the appropriate fragment, no actions are taken
- startFileFragment(savedInstanceState);
-
- // no immediate actions!
- startListFragment(savedInstanceState, null, null, null, null);
- break;
- }
case ACTION_SEARCH_KEYSERVER_FROM_URL: {
// need to process URL to get search query and keyserver authority
String query = dataUri.getQueryParameter("search");
@@ -223,120 +212,88 @@ public class ImportKeysActivity extends BaseNfcActivity
// if query not specified, we still allow users to search the keyserver in the link
if (query == null) {
Notify.create(this, R.string.import_url_warn_no_search_parameter, Notify.LENGTH_INDEFINITE,
- Notify.Style.WARN).show(mTopFragment);
+ Notify.Style.WARN).show();
// we just set the keyserver
- startCloudFragment(savedInstanceState, null, false, keyserver);
+ startTopCloudFragment(null, false, keyserver);
// we don't set the keyserver for ImportKeysListFragment since
// it'll be set in the cloudSearchPrefs of ImportKeysCloudFragment
// which is used when the user clicks on the search button
- startListFragment(savedInstanceState, null, null, null, null);
+ startListFragment(null, null, null, null);
} else {
// we allow our users to edit the query if they wish
- startCloudFragment(savedInstanceState, query, false, keyserver);
+ startTopCloudFragment(query, false, keyserver);
// search immediately
- startListFragment(savedInstanceState, null, null, query, keyserver);
+ startListFragment(null, null, query, keyserver);
}
break;
}
+ case ACTION_IMPORT_KEY_FROM_FILE:
case ACTION_IMPORT_KEY_FROM_FILE_AND_RETURN: {
// NOTE: this only displays the appropriate fragment, no actions are taken
- startFileFragment(savedInstanceState);
-
- // no immediate actions!
- startListFragment(savedInstanceState, null, null, null, null);
+ startTopFileFragment();
+ startListFragment(null, null, null, null);
break;
}
default: {
- startCloudFragment(savedInstanceState, null, false, null);
- startListFragment(savedInstanceState, null, null, null, null);
+ startTopCloudFragment(null, false, null);
+ startListFragment(null, null, null, null);
break;
}
}
}
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+
+ // the only thing we need to take care of for restoring state is
+ // that the top layout is shown iff it contains a fragment
+ Fragment topFragment = getSupportFragmentManager().findFragmentByTag(TAG_FRAG_TOP);
+ boolean hasTopFragment = topFragment != null;
+ findViewById(R.id.import_keys_top_layout).setVisibility(hasTopFragment ? View.VISIBLE : View.GONE);
+ }
/**
* if the fragment is started with non-null bytes/dataUri/serverQuery, it will immediately
* load content
*
- * @param savedInstanceState
* @param bytes bytes containing list of keyrings to import
* @param dataUri uri to file to import keyrings from
* @param serverQuery query to search for on the keyserver
* @param keyserver keyserver authority to search on. If null will use keyserver from
* user preferences
*/
- private void startListFragment(Bundle savedInstanceState, byte[] bytes, Uri dataUri,
- String serverQuery, String keyserver) {
- // 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 (mListFragment != null) {
- return;
- }
-
- mListFragment = ImportKeysListFragment.newInstance(bytes, dataUri, serverQuery, false,
- keyserver);
-
- // Add the fragment to the 'fragment_container' FrameLayout
- // NOTE: We use commitAllowingStateLoss() to prevent weird crashes!
+ private void startListFragment(byte[] bytes, Uri dataUri, String serverQuery, String keyserver) {
+ Fragment listFragment =
+ ImportKeysListFragment.newInstance(bytes, dataUri, serverQuery, false, keyserver);
getSupportFragmentManager().beginTransaction()
- .replace(R.id.import_keys_list_container, mListFragment)
- .commitAllowingStateLoss();
- // do it immediately!
- getSupportFragmentManager().executePendingTransactions();
+ .replace(R.id.import_keys_list_container, listFragment, TAG_FRAG_LIST)
+ .commit();
}
- private void startFileFragment(Bundle savedInstanceState) {
- // 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 (mTopFragment != null) {
- return;
- }
-
- // Create an instance of the fragment
- mTopFragment = ImportKeysFileFragment.newInstance();
-
- // Add the fragment to the 'fragment_container' FrameLayout
- // NOTE: We use commitAllowingStateLoss() to prevent weird crashes!
+ private void startTopFileFragment() {
+ findViewById(R.id.import_keys_top_layout).setVisibility(View.VISIBLE);
+ Fragment importFileFragment = ImportKeysFileFragment.newInstance();
getSupportFragmentManager().beginTransaction()
- .replace(R.id.import_keys_top_container, mTopFragment)
- .commitAllowingStateLoss();
- // do it immediately!
- getSupportFragmentManager().executePendingTransactions();
+ .replace(R.id.import_keys_top_container, importFileFragment, TAG_FRAG_TOP)
+ .commit();
}
/**
* loads the CloudFragment, which consists of the search bar, search button and settings icon
* visually.
*
- * @param savedInstanceState
* @param query search query
* @param disableQueryEdit if true, user will not be able to edit the search query
* @param keyserver keyserver authority to use for search, if null will use keyserver
* specified in user preferences
*/
-
- private void startCloudFragment(Bundle savedInstanceState, String query, boolean disableQueryEdit, String
- keyserver) {
- // 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 (mTopFragment != null) {
- return;
- }
-
- // Create an instance of the fragment
- mTopFragment = ImportKeysCloudFragment.newInstance(query, disableQueryEdit, keyserver);
-
- // Add the fragment to the 'fragment_container' FrameLayout
- // NOTE: We use commitAllowingStateLoss() to prevent weird crashes!
+ private void startTopCloudFragment(String query, boolean disableQueryEdit, String keyserver) {
+ findViewById(R.id.import_keys_top_layout).setVisibility(View.VISIBLE);
+ Fragment importCloudFragment = ImportKeysCloudFragment.newInstance(query, disableQueryEdit, keyserver);
getSupportFragmentManager().beginTransaction()
- .replace(R.id.import_keys_top_container, mTopFragment)
- .commitAllowingStateLoss();
- // do it immediately!
- getSupportFragmentManager().executePendingTransactions();
+ .replace(R.id.import_keys_top_container, importCloudFragment, TAG_FRAG_TOP)
+ .commit();
}
private boolean isFingerprintValid(String fingerprint) {
@@ -350,63 +307,32 @@ public class ImportKeysActivity extends BaseNfcActivity
}
public void loadCallback(final ImportKeysListFragment.LoaderState loaderState) {
- mListFragment.loadNew(loaderState);
+ FragmentManager fragMan = getSupportFragmentManager();
+ ImportKeysListFragment keyListFragment = (ImportKeysListFragment) fragMan.findFragmentByTag(TAG_FRAG_LIST);
+ keyListFragment.loadNew(loaderState);
}
- private void handleMessage(Message message) {
- if (message.arg1 == ServiceProgressHandler.MessageStatus.OKAY.ordinal()) {
- // get returned data bundle
- Bundle returnData = message.getData();
- if (returnData == null) {
- return;
- }
- final ImportKeyResult result =
- returnData.getParcelable(OperationResult.EXTRA_RESULT);
- if (result == null) {
- Log.e(Constants.TAG, "result == null");
- return;
- }
+ private void importSelectedKeys() {
- if (ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_RESULT.equals(getIntent().getAction())
- || ACTION_IMPORT_KEY_FROM_FILE_AND_RETURN.equals(getIntent().getAction())) {
- Intent intent = new Intent();
- intent.putExtra(ImportKeyResult.EXTRA_RESULT, result);
- ImportKeysActivity.this.setResult(RESULT_OK, intent);
- ImportKeysActivity.this.finish();
- return;
- }
- if (ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_TO_SERVICE.equals(getIntent().getAction())) {
- ImportKeysActivity.this.setResult(RESULT_OK, mPendingIntentData);
- ImportKeysActivity.this.finish();
- return;
- }
-
- result.createNotify(ImportKeysActivity.this)
- .show((ViewGroup) findViewById(R.id.import_snackbar));
- }
- }
+ FragmentManager fragMan = getSupportFragmentManager();
+ ImportKeysListFragment keyListFragment = (ImportKeysListFragment) fragMan.findFragmentByTag(TAG_FRAG_LIST);
- /**
- * Import keys with mImportData
- */
- public void importKeys() {
-
- if (mListFragment.getSelectedEntries().size() == 0) {
+ if (keyListFragment.getSelectedEntries().size() == 0) {
Notify.create(this, R.string.error_nothing_import_selected, Notify.Style.ERROR)
.show((ViewGroup) findViewById(R.id.import_snackbar));
return;
}
- mOperationHelper = new CryptoOperationHelper<ImportKeyringParcel, ImportKeyResult>(
+ mOperationHelper = new CryptoOperationHelper<>(
1, this, this, R.string.progress_importing
);
- ImportKeysListFragment.LoaderState ls = mListFragment.getLoaderState();
+ ImportKeysListFragment.LoaderState ls = keyListFragment.getLoaderState();
if (ls instanceof ImportKeysListFragment.BytesLoaderState) {
Log.d(Constants.TAG, "importKeys started");
// get DATA from selected key entries
- IteratorWithSize<ParcelableKeyRing> selectedEntries = mListFragment.getSelectedData();
+ IteratorWithSize<ParcelableKeyRing> selectedEntries = keyListFragment.getSelectedData();
// instead of giving the entries by Intent extra, cache them into a
// file to prevent Java Binder problems on heavy imports
@@ -435,7 +361,7 @@ public class ImportKeysActivity extends BaseNfcActivity
ArrayList<ParcelableKeyRing> keys = new ArrayList<>();
{
// change the format into ParcelableKeyRing
- ArrayList<ImportKeysListEntry> entries = mListFragment.getSelectedEntries();
+ ArrayList<ImportKeysListEntry> entries = keyListFragment.getSelectedEntries();
for (ImportKeysListEntry entry : entries) {
keys.add(new ParcelableKeyRing(
entry.getFingerprintHex(), entry.getKeyIdHex(), entry.getExtraData())
@@ -458,24 +384,28 @@ public class ImportKeysActivity extends BaseNfcActivity
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (mOperationHelper == null ||
- !mOperationHelper.handleActivityResult(requestCode, resultCode, data)) {
- super.onActivityResult(requestCode, resultCode, data);
+ if (mOperationHelper != null &&
+ mOperationHelper.handleActivityResult(requestCode, resultCode, data)) {
+ return;
}
+ super.onActivityResult(requestCode, resultCode, data);
}
public void handleResult(ImportKeyResult result) {
- if (ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_RESULT.equals(getIntent().getAction())
- || ACTION_IMPORT_KEY_FROM_FILE_AND_RETURN.equals(getIntent().getAction())) {
+ String intentAction = getIntent().getAction();
+
+ if (ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_RESULT.equals(intentAction)
+ || ACTION_IMPORT_KEY_FROM_FILE_AND_RETURN.equals(intentAction)) {
Intent intent = new Intent();
intent.putExtra(ImportKeyResult.EXTRA_RESULT, result);
- ImportKeysActivity.this.setResult(RESULT_OK, intent);
- ImportKeysActivity.this.finish();
+ setResult(RESULT_OK, intent);
+ finish();
return;
}
- if (ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_TO_SERVICE.equals(getIntent().getAction())) {
- ImportKeysActivity.this.setResult(RESULT_OK, mPendingIntentData);
- ImportKeysActivity.this.finish();
+
+ if (ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_TO_SERVICE.equals(intentAction)) {
+ setResult(RESULT_OK, mPendingIntentData);
+ finish();
return;
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java
index 6f5d98afd..a5bd84d7e 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java
@@ -204,7 +204,7 @@ public class MainActivity extends BaseNfcActivity implements FabContainer, OnBac
private void onBackupSelected() {
mToolbar.setTitle(R.string.nav_backup);
mDrawer.setSelectionByIdentifier(ID_APPS, false);
- Fragment frag = new BackupFragment();
+ Fragment frag = new DrawerBackupFragment();
setFragment(frag, true);
}
@@ -265,7 +265,7 @@ public class MainActivity extends BaseNfcActivity implements FabContainer, OnBac
} else if (frag instanceof AppsListFragment) {
mToolbar.setTitle(R.string.nav_apps);
mDrawer.setSelection(mDrawer.getPositionFromIdentifier(ID_APPS), false);
- } else if (frag instanceof BackupFragment) {
+ } else if (frag instanceof DrawerBackupFragment) {
mToolbar.setTitle(R.string.nav_backup);
mDrawer.setSelection(mDrawer.getPositionFromIdentifier(ID_BACKUP), false);
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java
index 0415128a2..f38e4928d 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java
@@ -17,6 +17,7 @@
package org.sufficientlysecure.keychain.ui;
+
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
@@ -29,10 +30,12 @@ import android.widget.Spinner;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
-import org.sufficientlysecure.keychain.operations.results.ExportResult;
+import org.sufficientlysecure.keychain.operations.results.UploadResult;
+import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
-import org.sufficientlysecure.keychain.service.ExportKeyringParcel;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.service.UploadKeyringParcel;
import org.sufficientlysecure.keychain.ui.base.BaseActivity;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
import org.sufficientlysecure.keychain.util.Log;
@@ -42,7 +45,7 @@ import org.sufficientlysecure.keychain.util.Preferences;
* Sends the selected public key to a keyserver
*/
public class UploadKeyActivity extends BaseActivity
- implements CryptoOperationHelper.Callback<ExportKeyringParcel, ExportResult> {
+ implements CryptoOperationHelper.Callback<UploadKeyringParcel, UploadResult> {
private View mUploadButton;
private Spinner mKeyServerSpinner;
@@ -50,8 +53,8 @@ public class UploadKeyActivity extends BaseActivity
// CryptoOperationHelper.Callback vars
private String mKeyserver;
- private Uri mUnifiedKeyringUri;
- private CryptoOperationHelper<ExportKeyringParcel, ExportResult> mUploadOpHelper;
+ private long mMasterKeyId;
+ private CryptoOperationHelper<UploadKeyringParcel, UploadResult> mUploadOpHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -85,6 +88,16 @@ public class UploadKeyActivity extends BaseActivity
finish();
return;
}
+
+ try {
+ mMasterKeyId = new ProviderHelper(this).getCachedPublicKeyRing(
+ KeyRings.buildUnifiedKeyRingUri(mDataUri)).getMasterKeyId();
+ } catch (PgpKeyNotFoundException e) {
+ Log.e(Constants.TAG, "Intent data pointed to bad key!");
+ finish();
+ return;
+ }
+
}
@Override
@@ -101,13 +114,10 @@ public class UploadKeyActivity extends BaseActivity
}
private void uploadKey() {
- Uri blobUri = KeyRings.buildUnifiedKeyRingUri(mDataUri);
- mUnifiedKeyringUri = blobUri;
-
String server = (String) mKeyServerSpinner.getSelectedItem();
mKeyserver = server;
- mUploadOpHelper = new CryptoOperationHelper(1, this, this, R.string.progress_uploading);
+ mUploadOpHelper = new CryptoOperationHelper<>(1, this, this, R.string.progress_uploading);
mUploadOpHelper.cryptoOperation();
}
@@ -125,12 +135,12 @@ public class UploadKeyActivity extends BaseActivity
}
@Override
- public ExportKeyringParcel createOperationInput() {
- return new ExportKeyringParcel(mKeyserver, mUnifiedKeyringUri);
+ public UploadKeyringParcel createOperationInput() {
+ return new UploadKeyringParcel(mKeyserver, mMasterKeyId);
}
@Override
- public void onCryptoOperationSuccess(ExportResult result) {
+ public void onCryptoOperationSuccess(UploadResult result) {
result.createNotify(this).show();
}
@@ -140,7 +150,7 @@ public class UploadKeyActivity extends BaseActivity
}
@Override
- public void onCryptoOperationError(ExportResult result) {
+ public void onCryptoOperationError(UploadResult result) {
result.createNotify(this).show();
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java
index de859724b..48edbcbd6 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java
@@ -65,12 +65,14 @@ import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
+import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType;
import org.sufficientlysecure.keychain.pgp.KeyRing;
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException;
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
import org.sufficientlysecure.keychain.ui.ViewKeyFragment.PostponeType;
import org.sufficientlysecure.keychain.ui.base.BaseNfcActivity;
@@ -345,7 +347,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements
startActivity(homeIntent);
return true;
}
- case R.id.menu_key_view_export_file: {
+ case R.id.menu_key_view_backup: {
startPassphraseActivity(REQUEST_BACKUP);
return true;
}
@@ -395,7 +397,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements
MenuItem editKey = menu.findItem(R.id.menu_key_view_edit);
editKey.setVisible(mIsSecret);
- MenuItem exportKey = menu.findItem(R.id.menu_key_view_export_file);
+ MenuItem exportKey = menu.findItem(R.id.menu_key_view_backup);
exportKey.setVisible(mIsSecret);
MenuItem addLinked = menu.findItem(R.id.menu_key_view_add_linked_identity);
@@ -450,15 +452,40 @@ public class ViewKeyActivity extends BaseNfcActivity implements
}
private void startPassphraseActivity(int requestCode) {
- Intent intent = new Intent(this, PassphraseDialogActivity.class);
- intent.putExtra(PassphraseDialogActivity.EXTRA_SUBKEY_ID, mMasterKeyId);
- startActivityForResult(intent, requestCode);
+
+ if (keyHasPassphrase()) {
+ Intent intent = new Intent(this, PassphraseDialogActivity.class);
+ intent.putExtra(PassphraseDialogActivity.EXTRA_SUBKEY_ID, mMasterKeyId);
+ startActivityForResult(intent, requestCode);
+ } else {
+ startBackupActivity();
+ }
+ }
+
+ private boolean keyHasPassphrase() {
+ try {
+ SecretKeyType secretKeyType =
+ mProviderHelper.getCachedPublicKeyRing(mMasterKeyId).getSecretKeyType(mMasterKeyId);
+ switch (secretKeyType) {
+ // all of these make no sense to ask
+ case PASSPHRASE_EMPTY:
+ case GNU_DUMMY:
+ case DIVERT_TO_CARD:
+ case UNAVAILABLE:
+ return false;
+ default:
+ return true;
+ }
+ } catch (NotFoundException e) {
+ return false;
+ }
}
- private void backupToFile() {
- new ExportHelper(this).showExportKeysDialog(
- mMasterKeyId, new File(Constants.Path.APP_DIR,
- KeyFormattingUtils.convertKeyIdToHex(mMasterKeyId) + ".sec.asc"), true);
+ private void startBackupActivity() {
+ Intent intent = new Intent(this, BackupActivity.class);
+ intent.putExtra(BackupActivity.EXTRA_MASTER_KEY_IDS, new long[] { mMasterKeyId });
+ intent.putExtra(BackupActivity.EXTRA_SECRET, true);
+ startActivity(intent);
}
private void deleteKey() {
@@ -514,7 +541,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements
return;
}
- backupToFile();
+ startBackupActivity();
return;
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java
index 44323543f..a320ea3b2 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java
@@ -133,10 +133,7 @@ public class LinkedIdCreateHttpsStep2Fragment extends LinkedIdCreateFinalFragmen
String targetName = "pgpkey.txt";
// TODO: not supported on Android < 4.4
- FileHelper.saveDocument(this,
- targetName,
- "text/plain",
- REQUEST_CODE_OUTPUT);
+ FileHelper.saveDocument(this, targetName, "text/plain", REQUEST_CODE_OUTPUT);
}
private void saveFile(Uri uri) {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Notify.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Notify.java
index 7dfd56430..71f6ecc1a 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Notify.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Notify.java
@@ -37,7 +37,7 @@ import org.sufficientlysecure.keychain.util.FabContainer;
*/
public class Notify {
- public static enum Style {
+ public enum Style {
OK (R.color.android_green_light), WARN(R.color.android_orange_light), ERROR(R.color.android_red_light);
public final int mLineColor;
@@ -142,6 +142,11 @@ public class Notify {
return create(activity, text, LENGTH_LONG, style);
}
+ public static Showable create(Activity activity, int textResId, Style style,
+ ActionListener actionListener, int actionResId) {
+ return create(activity, textResId, LENGTH_LONG, style, actionListener, actionResId);
+ }
+
public static Showable create(Activity activity, int textResId, int duration, Style style,
ActionListener actionListener, int actionResId) {
return create(activity, activity.getString(textResId), duration, style, actionListener, actionResId);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java
index 18e830139..a8274e45a 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java
@@ -31,6 +31,7 @@ import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
+import android.view.animation.Animation;
import android.widget.ViewAnimator;
import org.sufficientlysecure.keychain.R;
@@ -73,4 +74,29 @@ public class ToolableViewAnimator extends ViewAnimator {
}
super.addView(child, index, params);
}
+
+ @Override
+ public void setDisplayedChild(int whichChild) {
+ if (whichChild != getDisplayedChild()) {
+ super.setDisplayedChild(whichChild);
+ }
+ }
+
+ public void setDisplayedChild(int whichChild, boolean animate) {
+ if (animate) {
+ setDisplayedChild(whichChild);
+ return;
+ }
+
+ Animation savedInAnim = getInAnimation();
+ Animation savedOutAnim = getOutAnimation();
+ setInAnimation(null);
+ setOutAnimation(null);
+
+ setDisplayedChild(whichChild);
+
+ setInAnimation(savedInAnim);
+ setOutAnimation(savedOutAnim);
+ }
+
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java
index f2ce456f6..cc90c173f 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java
@@ -97,7 +97,7 @@ public class ExportHelper
@Override
public ExportKeyringParcel createOperationInput() {
- return new ExportKeyringParcel(mMasterKeyIds, mExportSecret, mExportFile.getAbsolutePath());
+ return new ExportKeyringParcel(null, mMasterKeyIds, mExportSecret, Uri.fromFile(mExportFile));
}
@Override
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java
index feeaf3459..671275823 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java
@@ -55,7 +55,6 @@ import org.sufficientlysecure.keychain.R;
import static android.system.OsConstants.S_IROTH;
-
/** This class offers a number of helper functions for saving documents.
*
* There are three entry points here: openDocument, saveDocument and
@@ -82,24 +81,27 @@ import static android.system.OsConstants.S_IROTH;
*/
public class FileHelper {
- public static void openDocument(Fragment fragment, Uri last, String mimeType, boolean multiple, int requestCode) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
- openDocumentKitKat(fragment, mimeType, multiple, requestCode);
- } else {
- openDocumentPreKitKat(fragment, last, mimeType, multiple, requestCode);
- }
- }
-
+ @TargetApi(VERSION_CODES.KITKAT)
public static void saveDocument(Fragment fragment, String targetName, int requestCode) {
saveDocument(fragment, targetName, "*/*", requestCode);
}
- public static void saveDocument(Fragment fragment, String targetName, String mimeType,
- int requestCode) {
+ /** Opens the storage browser on Android 4.4 or later for saving a file. */
+ @TargetApi(VERSION_CODES.KITKAT)
+ public static void saveDocument(Fragment fragment, String mimeType, String suggestedName, int requestCode) {
+ Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.setType(mimeType);
+ intent.putExtra("android.content.extra.SHOW_ADVANCED", true); // Note: This is not documented, but works
+ intent.putExtra(Intent.EXTRA_TITLE, suggestedName);
+ fragment.startActivityForResult(intent, requestCode);
+ }
+
+ public static void openDocument(Fragment fragment, Uri last, String mimeType, boolean multiple, int requestCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
- saveDocumentKitKat(fragment, mimeType, targetName, requestCode);
+ openDocumentKitKat(fragment, mimeType, multiple, requestCode);
} else {
- throw new RuntimeException("saveDocument does not support Android < 4.4!");
+ openDocumentPreKitKat(fragment, last, mimeType, multiple, requestCode);
}
}
@@ -136,17 +138,6 @@ public class FileHelper {
fragment.startActivityForResult(intent, requestCode);
}
- /** Opens the storage browser on Android 4.4 or later for saving a file. */
- @TargetApi(Build.VERSION_CODES.KITKAT)
- public static void saveDocumentKitKat(Fragment fragment, String mimeType, String suggestedName, int requestCode) {
- Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
- intent.addCategory(Intent.CATEGORY_OPENABLE);
- intent.setType(mimeType);
- intent.putExtra("android.content.extra.SHOW_ADVANCED", true); // Note: This is not documented, but works
- intent.putExtra(Intent.EXTRA_TITLE, suggestedName);
- fragment.startActivityForResult(intent, requestCode);
- }
-
public static String getFilename(Context context, Uri uri) {
String filename = null;
try {
diff --git a/OpenKeychain/src/main/res/anim/fade_in_delayed.xml b/OpenKeychain/src/main/res/anim/fade_in_delayed.xml
new file mode 100644
index 000000000..3f2887b5e
--- /dev/null
+++ b/OpenKeychain/src/main/res/anim/fade_in_delayed.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <alpha android:fromAlpha="0.0" android:toAlpha="1.0"
+ android:interpolator="@android:anim/bounce_interpolator"
+ android:duration="700"
+ android:startOffset="400"
+ />
+</set>
diff --git a/OpenKeychain/src/main/res/anim/fade_out_delayed.xml b/OpenKeychain/src/main/res/anim/fade_out_delayed.xml
new file mode 100644
index 000000000..a895bdedd
--- /dev/null
+++ b/OpenKeychain/src/main/res/anim/fade_out_delayed.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <alpha android:fromAlpha="1.0" android:toAlpha="0.0"
+ android:interpolator="@android:anim/accelerate_interpolator"
+ android:duration="300"
+ android:startOffset="400"
+ />
+</set>
diff --git a/OpenKeychain/src/main/res/layout-land/backup_code_fragment.xml b/OpenKeychain/src/main/res/layout-land/backup_code_fragment.xml
new file mode 100644
index 000000000..fd8cd21f0
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout-land/backup_code_fragment.xml
@@ -0,0 +1,364 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:custom="http://schemas.android.com/apk/res-auto"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingTop="20dp">
+
+ <org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/title_animator"
+ android:inAnimation="@anim/fade_in"
+ android:outAnimation="@anim/fade_out"
+ custom:initialView="0">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="10dp"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center_horizontal"
+ android:text="@string/backup_code_explanation"
+ style="?android:textAppearanceMedium"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="10dp"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center_horizontal"
+ android:text="@string/backup_code_enter"
+ style="?android:textAppearanceMedium"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="10dp"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center_horizontal"
+ android:text="@string/backup_code_ok"
+ style="?android:textAppearanceMedium"
+ />
+
+ </org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator>
+
+ <org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/code_animator"
+ android:inAnimation="@anim/fade_in"
+ android:outAnimation="@anim/fade_out"
+ android:layout_marginTop="15dp"
+ android:layout_marginBottom="15dp"
+ custom:initialView="1">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ >
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/backup_code_display_1"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ style="@android:style/Widget.EditText"
+ android:clickable="false"
+ android:focusable="false"
+ tools:text="ABCDEF"
+ tools:ignore="SpUsage"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ android:text="-"
+ tools:ignore="HardcodedText,SpUsage" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/backup_code_display_2"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ style="@android:style/Widget.EditText"
+ android:clickable="false"
+ android:focusable="false"
+ tools:text="GHIJKL"
+ tools:ignore="SpUsage" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ android:text="-"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/backup_code_display_3"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ style="@android:style/Widget.EditText"
+ android:clickable="false"
+ android:focusable="false"
+ tools:text="MNOPQR"
+ tools:ignore="SpUsage"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ android:text="-"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/backup_code_display_4"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ android:singleLine="true"
+ style="@android:style/Widget.EditText"
+ android:clickable="false"
+ android:focusable="false"
+ tools:text="STUVWX"
+ tools:ignore="SpUsage"
+ />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ >
+
+ <!--
+ The most reliable way to correctly size these I found was to put a transparent hint on them.
+ Theoretically, this should be what the android:ems attribute is for - didn't work for me.
+ -->
+ <EditText
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/backup_code_1"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ android:singleLine="true"
+ android:inputType="textNoSuggestions|textCapCharacters"
+ android:hint="______"
+ android:textColorHint="@android:color/transparent"
+ android:maxLength="6"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ android:text="-"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+
+ <EditText
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/backup_code_2"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ android:singleLine="true"
+ android:inputType="textNoSuggestions|textCapCharacters"
+ android:hint="______"
+ android:textColorHint="@android:color/transparent"
+ android:maxLength="6"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ android:text="-"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+
+ <EditText
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/backup_code_3"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ android:singleLine="true"
+ android:inputType="textNoSuggestions|textCapCharacters"
+ android:hint="______"
+ android:textColorHint="@android:color/transparent"
+ android:maxLength="6"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ android:text="-"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+
+ <EditText
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/backup_code_4"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ android:singleLine="true"
+ android:inputType="textNoSuggestions|textCapCharacters"
+ android:hint="______"
+ android:textColorHint="@android:color/transparent"
+ android:maxLength="6"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+
+ </LinearLayout>
+
+ </org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator>
+
+ <org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/status_animator"
+ android:inAnimation="@anim/fade_in_delayed"
+ android:outAnimation="@anim/fade_out"
+ custom:initialView="2">
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:layout_margin="10dp"
+ android:text="@string/btn_code_wrotedown"
+ android:drawableLeft="@drawable/ic_mode_edit_grey_24dp"
+ android:drawablePadding="8dp"
+ android:padding="12dp"
+ android:id="@+id/button_backup_input"
+ style="?android:buttonBarButtonStyle"
+ />
+
+ <Space
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:orientation="horizontal">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:text="@string/backup_code_wrong"
+ style="?android:textAppearanceMedium"
+ />
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="10dp"
+ android:layout_gravity="center_horizontal"
+ android:padding="12dp"
+ android:text="@string/btn_backup_back"
+ android:drawableLeft="@drawable/ic_repeat_grey_24dp"
+ android:drawablePadding="8dp"
+ android:id="@+id/button_backup_back"
+ style="?android:buttonBarButtonStyle"
+ />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ style="?android:buttonBarStyle">
+
+ <Button
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_margin="10dp"
+ android:padding="12dp"
+ android:text="@string/btn_backup_share"
+ android:drawableLeft="@drawable/ic_share_grey_24dp"
+ android:drawablePadding="8dp"
+ android:id="@+id/button_backup_share"
+ style="?android:buttonBarButtonStyle"
+ />
+
+ <Button
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_margin="10dp"
+ android:padding="12dp"
+ android:text="@string/btn_backup_save"
+ android:drawableLeft="@drawable/ic_save_grey_24dp"
+ android:drawablePadding="8dp"
+ android:id="@+id/button_backup_save"
+ style="?android:buttonBarButtonStyle"
+ />
+
+ </LinearLayout>
+
+ </org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator>
+
+
+</LinearLayout>
diff --git a/OpenKeychain/src/main/res/layout-land/qr_code_activity.xml b/OpenKeychain/src/main/res/layout-land/qr_code_activity.xml
deleted file mode 100644
index 59a733e89..000000000
--- a/OpenKeychain/src/main/res/layout-land/qr_code_activity.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <include
- android:id="@+id/toolbar_include"
- layout="@layout/toolbar_standalone" />
-
- <LinearLayout
- android:layout_below="@id/toolbar_include"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical">
-
- <android.support.v7.widget.CardView
- android:id="@+id/qr_code_image_layout"
- android:transitionName="qr_code"
- android:layout_gravity="center_horizontal"
- android:layout_margin="32dp"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:clickable="true"
- android:foreground="?android:attr/selectableItemBackground"
- app:cardBackgroundColor="@android:color/white"
- app:cardUseCompatPadding="true"
- app:cardCornerRadius="4dp">
-
- <org.sufficientlysecure.keychain.ui.widget.AspectRatioImageView
- android:id="@+id/qr_code_image"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- app:dominantMeasurement="height"
- app:aspectRatioEnabled="true" />
- </android.support.v7.widget.CardView>
-
- </LinearLayout>
-</RelativeLayout> \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/layout-mdpi/backup_code_fragment.xml b/OpenKeychain/src/main/res/layout-mdpi/backup_code_fragment.xml
new file mode 100644
index 000000000..4c7cb7374
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout-mdpi/backup_code_fragment.xml
@@ -0,0 +1,363 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:custom="http://schemas.android.com/apk/res-auto"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingTop="50dp">
+
+ <org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/title_animator"
+ android:inAnimation="@anim/fade_in"
+ android:outAnimation="@anim/fade_out"
+ custom:initialView="0">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="10dp"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center_horizontal"
+ android:text="@string/backup_code_explanation"
+ style="?android:textAppearanceMedium"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="10dp"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center_horizontal"
+ android:text="@string/backup_code_enter"
+ style="?android:textAppearanceMedium"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="10dp"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center_horizontal"
+ android:text="@string/backup_code_ok"
+ style="?android:textAppearanceMedium"
+ />
+
+ </org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator>
+
+ <org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/code_animator"
+ android:inAnimation="@anim/fade_in"
+ android:outAnimation="@anim/fade_out"
+ android:layout_marginTop="15dp"
+ android:layout_marginBottom="15dp"
+ custom:initialView="0">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ >
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/backup_code_display_1"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="16dp"
+ style="@android:style/Widget.EditText"
+ android:clickable="false"
+ android:focusable="false"
+ tools:text="ABCDEF"
+ tools:ignore="SpUsage"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="16dp"
+ android:text="-"
+ tools:ignore="HardcodedText,SpUsage" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/backup_code_display_2"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="16dp"
+ style="@android:style/Widget.EditText"
+ android:clickable="false"
+ android:focusable="false"
+ tools:text="GHIJKL"
+ tools:ignore="SpUsage"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="16dp"
+ android:text="-"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/backup_code_display_3"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="16dp"
+ style="@android:style/Widget.EditText"
+ android:clickable="false"
+ android:focusable="false"
+ tools:text="MNOPQR"
+ tools:ignore="SpUsage"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="16dp"
+ android:text="-"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/backup_code_display_4"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="16dp"
+ android:singleLine="true"
+ style="@android:style/Widget.EditText"
+ android:clickable="false"
+ android:focusable="false"
+ tools:text="STUVWX"
+ tools:ignore="SpUsage"
+ />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ >
+
+ <!--
+ The most reliable way to correctly size these I found was to put a transparent hint on them.
+ Theoretically, this should be what the android:ems attribute is for - didn't work for me.
+ -->
+ <EditText
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/backup_code_1"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="16dp"
+ android:singleLine="true"
+ android:inputType="textNoSuggestions|textCapCharacters"
+ android:hint="ABCDEF"
+ android:textColorHint="@android:color/transparent"
+ android:maxLength="6"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="16dp"
+ android:text="-"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+
+ <EditText
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/backup_code_2"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="16dp"
+ android:singleLine="true"
+ android:inputType="textNoSuggestions|textCapCharacters"
+ android:hint="ABCDEF"
+ android:textColorHint="@android:color/transparent"
+ android:maxLength="6"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="16dp"
+ android:text="-"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+
+ <EditText
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/backup_code_3"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="16dp"
+ android:singleLine="true"
+ android:inputType="textNoSuggestions|textCapCharacters"
+ android:hint="ABCDEF"
+ android:textColorHint="@android:color/transparent"
+ android:maxLength="6"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="16dp"
+ android:text="-"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+
+ <EditText
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/backup_code_4"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="16dp"
+ android:singleLine="true"
+ android:inputType="textNoSuggestions|textCapCharacters"
+ android:hint="ABCDEF"
+ android:textColorHint="@android:color/transparent"
+ android:maxLength="6"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+
+ </LinearLayout>
+
+ </org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator>
+
+ <org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/status_animator"
+ android:inAnimation="@anim/fade_in_delayed"
+ android:outAnimation="@anim/fade_out"
+ custom:initialView="2">
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:layout_margin="10dp"
+ android:text="@string/btn_code_wrotedown"
+ android:padding="12dp"
+ android:id="@+id/button_backup_input"
+ style="?android:buttonBarButtonStyle"
+ />
+
+ <Space
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:text="@string/backup_code_wrong"
+ style="?android:textAppearanceMedium"
+ />
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="10dp"
+ android:layout_gravity="center_horizontal"
+ android:padding="12dp"
+ android:text="@string/btn_backup_back"
+ android:drawableLeft="@drawable/ic_repeat_grey_24dp"
+ android:drawablePadding="8dp"
+ android:id="@+id/button_backup_back"
+ style="?android:buttonBarButtonStyle"
+ />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ style="?android:buttonBarStyle">
+
+ <Button
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_margin="10dp"
+ android:padding="12dp"
+ android:text="@string/btn_backup_share"
+ android:drawableLeft="@drawable/ic_share_grey_24dp"
+ android:drawablePadding="8dp"
+ android:id="@+id/button_backup_share"
+ style="?android:buttonBarButtonStyle"
+ />
+
+ <Button
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_margin="10dp"
+ android:padding="12dp"
+ android:text="@string/btn_backup_save"
+ android:drawableLeft="@drawable/ic_save_grey_24dp"
+ android:drawablePadding="8dp"
+ android:id="@+id/button_backup_save"
+ style="?android:buttonBarButtonStyle"
+ />
+
+ </LinearLayout>
+
+ </org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator>
+
+
+</LinearLayout>
diff --git a/OpenKeychain/src/main/res/layout-xhdpi/backup_code_fragment.xml b/OpenKeychain/src/main/res/layout-xhdpi/backup_code_fragment.xml
new file mode 100644
index 000000000..1ea976656
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout-xhdpi/backup_code_fragment.xml
@@ -0,0 +1,364 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:custom="http://schemas.android.com/apk/res-auto"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingTop="50dp">
+
+ <org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/title_animator"
+ android:inAnimation="@anim/fade_in"
+ android:outAnimation="@anim/fade_out"
+ custom:initialView="0">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="10dp"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center_horizontal"
+ android:text="@string/backup_code_explanation"
+ style="?android:textAppearanceMedium"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="10dp"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center_horizontal"
+ android:text="@string/backup_code_enter"
+ style="?android:textAppearanceMedium"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="10dp"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center_horizontal"
+ android:text="@string/backup_code_ok"
+ style="?android:textAppearanceMedium"
+ />
+
+ </org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator>
+
+ <org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/code_animator"
+ android:inAnimation="@anim/fade_in"
+ android:outAnimation="@anim/fade_out"
+ android:layout_marginTop="15dp"
+ android:layout_marginBottom="15dp"
+ custom:initialView="0">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ >
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/backup_code_display_1"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ style="@android:style/Widget.EditText"
+ android:clickable="false"
+ android:focusable="false"
+ tools:text="ABCDEF"
+ tools:ignore="SpUsage"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ android:text="-"
+ tools:ignore="HardcodedText,SpUsage" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/backup_code_display_2"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ style="@android:style/Widget.EditText"
+ android:clickable="false"
+ android:focusable="false"
+ tools:text="GHIJKL"
+ tools:ignore="SpUsage" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ android:text="-"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/backup_code_display_3"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ style="@android:style/Widget.EditText"
+ android:clickable="false"
+ android:focusable="false"
+ tools:text="MNOPQR"
+ tools:ignore="SpUsage"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ android:text="-"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/backup_code_display_4"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ android:singleLine="true"
+ style="@android:style/Widget.EditText"
+ android:clickable="false"
+ android:focusable="false"
+ tools:text="STUVWX"
+ tools:ignore="SpUsage"
+ />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ >
+
+ <!--
+ The most reliable way to correctly size these I found was to put a transparent hint on them.
+ Theoretically, this should be what the android:ems attribute is for - didn't work for me.
+ -->
+ <EditText
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/backup_code_1"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ android:singleLine="true"
+ android:inputType="textNoSuggestions|textCapCharacters"
+ android:hint="ABCDEF"
+ android:textColorHint="@android:color/transparent"
+ android:maxLength="6"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ android:text="-"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+
+ <EditText
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/backup_code_2"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ android:singleLine="true"
+ android:inputType="textNoSuggestions|textCapCharacters"
+ android:hint="ABCDEF"
+ android:textColorHint="@android:color/transparent"
+ android:maxLength="6"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ android:text="-"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+
+ <EditText
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/backup_code_3"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ android:singleLine="true"
+ android:inputType="textNoSuggestions|textCapCharacters"
+ android:hint="ABCDEF"
+ android:textColorHint="@android:color/transparent"
+ android:maxLength="6"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ android:text="-"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+
+ <EditText
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/backup_code_4"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ android:textSize="18dp"
+ android:singleLine="true"
+ android:inputType="textNoSuggestions|textCapCharacters"
+ android:hint="ABCDEF"
+ android:textColorHint="@android:color/transparent"
+ android:maxLength="6"
+ tools:ignore="HardcodedText,SpUsage"
+ />
+
+ </LinearLayout>
+
+ </org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator>
+
+ <org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:layout_gravity="center_horizontal"
+ android:id="@+id/status_animator"
+ android:inAnimation="@anim/fade_in_delayed"
+ android:outAnimation="@anim/fade_out"
+ custom:initialView="2">
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:layout_margin="10dp"
+ android:text="@string/btn_code_wrotedown"
+ android:drawableLeft="@drawable/ic_mode_edit_grey_24dp"
+ android:drawablePadding="8dp"
+ android:padding="12dp"
+ android:id="@+id/button_backup_input"
+ style="?android:buttonBarButtonStyle"
+ />
+
+ <Space
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:text="@string/backup_code_wrong"
+ style="?android:textAppearanceMedium"
+ />
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="10dp"
+ android:layout_gravity="center_horizontal"
+ android:padding="12dp"
+ android:text="@string/btn_backup_back"
+ android:drawableLeft="@drawable/ic_repeat_grey_24dp"
+ android:drawablePadding="8dp"
+ android:id="@+id/button_backup_back"
+ style="?android:buttonBarButtonStyle"
+ />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ style="?android:buttonBarStyle">
+
+ <Button
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_margin="10dp"
+ android:padding="12dp"
+ android:text="@string/btn_backup_share"
+ android:drawableLeft="@drawable/ic_share_grey_24dp"
+ android:drawablePadding="8dp"
+ android:id="@+id/button_backup_share"
+ style="?android:buttonBarButtonStyle"
+ />
+
+ <Button
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_margin="10dp"
+ android:padding="12dp"
+ android:text="@string/btn_backup_save"
+ android:drawableLeft="@drawable/ic_save_grey_24dp"
+ android:drawablePadding="8dp"
+ android:id="@+id/button_backup_save"
+ style="?android:buttonBarButtonStyle"
+ />
+
+ </LinearLayout>
+
+ </org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator>
+
+
+</LinearLayout>
diff --git a/OpenKeychain/src/main/res/layout/backup_activity.xml b/OpenKeychain/src/main/res/layout/backup_activity.xml
new file mode 100644
index 000000000..59ab6cbf2
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout/backup_activity.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <include
+ android:id="@+id/toolbar_include"
+ layout="@layout/toolbar_standalone_white" />
+
+ <LinearLayout
+ android:layout_below="@id/toolbar_include"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <include layout="@layout/notify_area" />
+
+ <FrameLayout
+ android:id="@+id/content_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ </FrameLayout>
+
+ </LinearLayout>
+</RelativeLayout> \ No newline at end of file
diff --git a/OpenKeychain/src/main/res/layout/decrypt_list_entry.xml b/OpenKeychain/src/main/res/layout/decrypt_list_entry.xml
index 7869b9a8a..4a79f0ccd 100644
--- a/OpenKeychain/src/main/res/layout/decrypt_list_entry.xml
+++ b/OpenKeychain/src/main/res/layout/decrypt_list_entry.xml
@@ -65,34 +65,35 @@
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:paddingRight="4dp"
- android:paddingLeft="4dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="4dp"
+ android:gravity="center_vertical"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/result_encryption_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:padding="4dp"
android:src="@drawable/status_lock_open_24dp"
- android:layout_gravity="center_vertical" />
+ />
<TextView
android:id="@+id/result_encryption_text"
android:layout_width="0dp"
- android:layout_height="wrap_content"
android:layout_weight="1"
+ android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
- android:layout_marginLeft="8dp"
- android:layout_marginTop="8dp"
- android:layout_marginBottom="8dp"
+ android:layout_marginLeft="6dp"
android:text=""
tools:text="Encryption status text" />
<ImageView
android:id="@+id/context_menu"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="4dp"
android:scaleType="center"
- android:layout_width="36dip"
- android:layout_height="48dip"
android:clickable="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_more_vert_black_24dp" />
@@ -103,25 +104,25 @@
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:paddingRight="4dp"
- android:paddingLeft="4dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="4dp"
+ android:gravity="center_vertical"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/result_signature_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:padding="4dp"
android:src="@drawable/status_signature_unverified_cutout_24dp"
- android:layout_gravity="center_vertical" />
+ />
<TextView
android:id="@+id/result_signature_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
- android:layout_marginLeft="8dp"
- android:layout_marginTop="8dp"
- android:layout_marginBottom="8dp"
+ android:layout_marginLeft="6dp"
android:text=""
tools:text="Signature status text" />
</LinearLayout>
@@ -130,10 +131,11 @@
android:id="@+id/result_signature_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:layout_marginTop="2dp"
+ android:layout_marginBottom="2dp"
android:clickable="true"
android:background="?android:selectableItemBackground"
android:orientation="horizontal"
- style="?listPreferredItemHeight"
>
<LinearLayout
diff --git a/OpenKeychain/src/main/res/layout/backup_fragment.xml b/OpenKeychain/src/main/res/layout/drawer_backup_fragment.xml
index 96fba954b..96fba954b 100644
--- a/OpenKeychain/src/main/res/layout/backup_fragment.xml
+++ b/OpenKeychain/src/main/res/layout/drawer_backup_fragment.xml
diff --git a/OpenKeychain/src/main/res/layout/import_keys_activity.xml b/OpenKeychain/src/main/res/layout/import_keys_activity.xml
index 28bb8a0b8..6602c4b8f 100644
--- a/OpenKeychain/src/main/res/layout/import_keys_activity.xml
+++ b/OpenKeychain/src/main/res/layout/import_keys_activity.xml
@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="match_parent"
+ xmlns:tools="http://schemas.android.com/tools">
<include
android:id="@+id/toolbar_include"
@@ -21,16 +22,26 @@
<include layout="@layout/notify_area" />
- <FrameLayout
- android:id="@+id/import_keys_top_container"
+ <LinearLayout
android:layout_width="match_parent"
- android:layout_height="64dp"
- android:orientation="vertical" />
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:id="@+id/import_keys_top_layout"
+ android:visibility="gone"
+ tools:visibility="visible">
- <View
- android:layout_width="match_parent"
- android:layout_height="1dip"
- android:background="?android:attr/listDivider" />
+ <FrameLayout
+ android:id="@+id/import_keys_top_container"
+ android:layout_width="match_parent"
+ android:layout_height="64dp"
+ android:orientation="vertical" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:background="?android:attr/listDivider" />
+
+ </LinearLayout>
<FrameLayout
android:id="@+id/import_keys_list_container"
diff --git a/OpenKeychain/src/main/res/menu/key_view.xml b/OpenKeychain/src/main/res/menu/key_view.xml
index 1bda1463a..c0adfcd6f 100644
--- a/OpenKeychain/src/main/res/menu/key_view.xml
+++ b/OpenKeychain/src/main/res/menu/key_view.xml
@@ -16,7 +16,7 @@
android:title="@string/key_view_action_update" />
<item
- android:id="@+id/menu_key_view_export_file"
+ android:id="@+id/menu_key_view_backup"
app:showAsAction="never"
android:title="@string/menu_export_key" />
diff --git a/OpenKeychain/src/main/res/values/strings.xml b/OpenKeychain/src/main/res/values/strings.xml
index 03493d697..8f86d5976 100644
--- a/OpenKeychain/src/main/res/values/strings.xml
+++ b/OpenKeychain/src/main/res/values/strings.xml
@@ -30,6 +30,7 @@
<string name="title_export_keys">"Backup Keys"</string>
<string name="title_key_not_found">"Key Not Found"</string>
<string name="title_send_key">"Upload to Keyserver"</string>
+ <string name="title_backup">"Backup Key"</string>
<string name="title_certify_key">"Confirm Key"</string>
<string name="title_key_details">"Key Details"</string>
<string name="title_help">"Help"</string>
@@ -107,7 +108,7 @@
<!-- menu -->
<string name="menu_preferences">"Settings"</string>
<string name="menu_help">"Help"</string>
- <string name="menu_export_key">"Backup to file"</string>
+ <string name="menu_export_key">"Backup key"</string>
<string name="menu_delete_key">"Delete key"</string>
<string name="menu_manage_keys">"Manage my keys"</string>
<string name="menu_search">"Search"</string>
@@ -1295,16 +1296,11 @@
<item quantity="one">"Exporting one key"</item>
<item quantity="other">"Exporting %d keys"</item>
</plurals>
- <string name="msg_export_file_name">"Filename: %s"</string>
<string name="msg_export_all">"Exporting all keys"</string>
<string name="msg_export_public">"Exporting public key %s"</string>
<string name="msg_export_upload_public">"Uploading public key %s"</string>
<string name="msg_export_secret">"Exporting secret key %s"</string>
- <string name="msg_export_error_no_file">"No filename specified!"</string>
- <string name="msg_export_error_fopen">"Error opening file!"</string>
- <string name="msg_export_error_no_uri">"No URI specified!"</string>
<string name="msg_export_error_uri_open">"Error opening URI stream!"</string>
- <string name="msg_export_error_storage">"Storage is not ready for writing!"</string>
<string name="msg_export_error_db">"Database error!"</string>
<string name="msg_export_error_io">"Input/output error!"</string>
<string name="msg_export_error_key">"Error preprocessing key data!"</string>
@@ -1555,6 +1551,7 @@
<string name="error_reading_k9">"Received incomplete data, try pressing 'Download complete message' in K-9 Mail!"</string>
<string name="filename_unknown">Unknown filename (click to open)</string>
<string name="filename_unknown_text">Text (click to show)</string>
+ <string name="filename_keys">"Key Backup (click to import)"</string>
<string name="intent_show">Show Signed/Encrypted Content</string>
<string name="intent_share">Share Signed/Encrypted Content</string>
<string name="view_internal">"View in OpenKeychain"</string>
@@ -1645,6 +1642,19 @@
<string name="linked_error_http">"Communication error: %s"</string>
<string name="linked_webview_title_github">"GitHub Authorization"</string>
<string name="linked_gist_description">"OpenKeychain API Tests"</string>
+ <string name="snack_btn_overwrite">"Overwrite"</string>
+ <string name="backup_code_explanation">"The backup will be secured with a backup code. Write it down before you proceed!"</string>
+ <string name="backup_code_enter">"Please enter the backup code:"</string>
+ <string name="backup_code_ok">"Code accepted!"</string>
+ <string name="btn_code_wrotedown">"OK, I wrote it down!"</string>
+ <string name="backup_code_wrong">"The backup code you entered is wrong!\nDid you write it down correctly?"</string>
+ <string name="btn_backup_share">"Share backup"</string>
+ <string name="btn_backup_save">"Save backup"</string>
+ <string name="snack_backup_error_saving">"Error saving backup!"</string>
+ <string name="snack_backup_saved">"Backup saved"</string>
+ <string name="snack_backup_exists">"Backup already exists!"</string>
+ <string name="snack_backup_saved_dir">"Saved to OpenKeychain directory"</string>
+ <string name="btn_backup_back">Go back to check</string>
<string name="share_log_dialog_title">"Share log?"</string>
<string name="share_log_dialog_message">"While logs can be super helpful for developers to find bugs in OpenKeychain, they can contain potential sensitive information such as data about the updated keys. Please make sure you are okay with sharing this information."</string>
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/ExportTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/ExportTest.java
index ae7c13c76..dfc1dd2c9 100644
--- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/ExportTest.java
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/ExportTest.java
@@ -17,40 +17,65 @@
package org.sufficientlysecure.keychain.operations;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.io.PrintStream;
+import java.security.Security;
+import java.util.Iterator;
+
+import android.app.Application;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.net.Uri;
+
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.robolectric.Robolectric;
import org.robolectric.RobolectricGradleTestRunner;
-import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLog;
import org.spongycastle.bcpg.sig.KeyFlags;
import org.spongycastle.jce.provider.BouncyCastleProvider;
import org.sufficientlysecure.keychain.WorkaroundBuildConfig;
-import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult;
+import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult;
import org.sufficientlysecure.keychain.operations.results.ExportResult;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult;
+import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel;
+import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation;
import org.sufficientlysecure.keychain.pgp.PgpKeyOperation;
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing.IteratorWithIOThrow;
import org.sufficientlysecure.keychain.pgp.WrappedSignature;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider;
+import org.sufficientlysecure.keychain.service.ExportKeyringParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel;
+import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.Passphrase;
import org.sufficientlysecure.keychain.util.ProgressScaler;
import org.sufficientlysecure.keychain.util.TestingUtils;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.PrintStream;
-import java.security.Security;
-import java.util.Iterator;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
@@ -84,7 +109,7 @@ public class ExportTest {
parcel.mNewUnlock = new ChangeUnlockParcel(mKeyPhrase1);
PgpEditKeyResult result = op.createSecretKeyRing(parcel);
- Assert.assertTrue("initial test key creation must succeed", result.success());
+ assertTrue("initial test key creation must succeed", result.success());
Assert.assertNotNull("initial test key creation must succeed", result.getRing());
mStaticRing1 = result.getRing();
@@ -102,7 +127,7 @@ public class ExportTest {
parcel.mNewUnlock = new ChangeUnlockParcel(null, new Passphrase("1234"));
PgpEditKeyResult result = op.createSecretKeyRing(parcel);
- Assert.assertTrue("initial test key creation must succeed", result.success());
+ assertTrue("initial test key creation must succeed", result.success());
Assert.assertNotNull("initial test key creation must succeed", result.getRing());
mStaticRing2 = result.getRing();
@@ -125,17 +150,17 @@ public class ExportTest {
}
@Test
- public void testExportAll() throws Exception {
+ public void testExportAllLocalStripped() throws Exception {
ExportOperation op = new ExportOperation(RuntimeEnvironment.application,
new ProviderHelper(RuntimeEnvironment.application), null);
// make sure there is a local cert (so the later checks that there are none are meaningful)
- Assert.assertTrue("second keyring has local certification", checkForLocal(mStaticRing2));
+ assertTrue("second keyring has local certification", checkForLocal(mStaticRing2));
ByteArrayOutputStream out = new ByteArrayOutputStream();
- ExportResult result = op.exportKeyRings(new OperationLog(), null, false, out);
+ boolean result = op.exportKeysToStream(new OperationLog(), null, false, out);
- Assert.assertTrue("export must be a success", result.success());
+ assertTrue("export must be a success", result);
long masterKeyId1, masterKeyId2;
if (mStaticRing1.getMasterKeyId() < mStaticRing2.getMasterKeyId()) {
@@ -150,70 +175,209 @@ public class ExportTest {
UncachedKeyRing.fromStream(new ByteArrayInputStream(out.toByteArray()));
{
- Assert.assertTrue("export must have two keys (1/2)", unc.hasNext());
+ assertTrue("export must have two keys (1/2)", unc.hasNext());
UncachedKeyRing ring = unc.next();
Assert.assertEquals("first exported key has correct masterkeyid",
masterKeyId1, ring.getMasterKeyId());
- Assert.assertFalse("first exported key must not be secret", ring.isSecret());
- Assert.assertFalse("there must be no local signatures in an exported keyring",
+ assertFalse("first exported key must not be secret", ring.isSecret());
+ assertFalse("there must be no local signatures in an exported keyring",
checkForLocal(ring));
}
{
- Assert.assertTrue("export must have two keys (2/2)", unc.hasNext());
+ assertTrue("export must have two keys (2/2)", unc.hasNext());
UncachedKeyRing ring = unc.next();
Assert.assertEquals("second exported key has correct masterkeyid",
masterKeyId2, ring.getMasterKeyId());
- Assert.assertFalse("second exported key must not be secret", ring.isSecret());
- Assert.assertFalse("there must be no local signatures in an exported keyring",
+ assertFalse("second exported key must not be secret", ring.isSecret());
+ assertFalse("there must be no local signatures in an exported keyring",
checkForLocal(ring));
}
out = new ByteArrayOutputStream();
- result = op.exportKeyRings(new OperationLog(), null, true, out);
+ result = op.exportKeysToStream(new OperationLog(), null, true, out);
- Assert.assertTrue("export must be a success", result.success());
+ assertTrue("export must be a success", result);
unc = UncachedKeyRing.fromStream(new ByteArrayInputStream(out.toByteArray()));
{
- Assert.assertTrue("export must have four keys (1/4)", unc.hasNext());
+ assertTrue("export must have four keys (1/4)", unc.hasNext());
UncachedKeyRing ring = unc.next();
Assert.assertEquals("1/4 exported key has correct masterkeyid",
masterKeyId1, ring.getMasterKeyId());
- Assert.assertFalse("1/4 exported key must not be public", ring.isSecret());
- Assert.assertFalse("there must be no local signatures in an exported keyring",
+ assertFalse("1/4 exported key must not be public", ring.isSecret());
+ assertFalse("there must be no local signatures in an exported keyring",
checkForLocal(ring));
- Assert.assertTrue("export must have four keys (2/4)", unc.hasNext());
+ assertTrue("export must have four keys (2/4)", unc.hasNext());
ring = unc.next();
Assert.assertEquals("2/4 exported key has correct masterkeyid",
masterKeyId1, ring.getMasterKeyId());
- Assert.assertTrue("2/4 exported key must be public", ring.isSecret());
- Assert.assertFalse("there must be no local signatures in an exported keyring",
+ assertTrue("2/4 exported key must be public", ring.isSecret());
+ assertFalse("there must be no local signatures in an exported keyring",
checkForLocal(ring));
}
{
- Assert.assertTrue("export must have four keys (3/4)", unc.hasNext());
+ assertTrue("export must have four keys (3/4)", unc.hasNext());
UncachedKeyRing ring = unc.next();
Assert.assertEquals("3/4 exported key has correct masterkeyid",
masterKeyId2, ring.getMasterKeyId());
- Assert.assertFalse("3/4 exported key must not be public", ring.isSecret());
- Assert.assertFalse("there must be no local signatures in an exported keyring",
+ assertFalse("3/4 exported key must not be public", ring.isSecret());
+ assertFalse("there must be no local signatures in an exported keyring",
checkForLocal(ring));
- Assert.assertTrue("export must have four keys (4/4)", unc.hasNext());
+ assertTrue("export must have four keys (4/4)", unc.hasNext());
ring = unc.next();
Assert.assertEquals("4/4 exported key has correct masterkeyid",
masterKeyId2, ring.getMasterKeyId());
- Assert.assertTrue("4/4 exported key must be public", ring.isSecret());
- Assert.assertFalse("there must be no local signatures in an exported keyring",
+ assertTrue("4/4 exported key must be public", ring.isSecret());
+ assertFalse("there must be no local signatures in an exported keyring",
checkForLocal(ring));
}
}
+ @Test
+ public void testExportUnencrypted() throws Exception {
+
+ ContentResolver mockResolver = mock(ContentResolver.class);
+
+ Uri fakeOutputUri = Uri.parse("content://fake/out/1");
+ ByteArrayOutputStream outStream1 = new ByteArrayOutputStream();
+ when(mockResolver.openOutputStream(fakeOutputUri)).thenReturn(outStream1);
+
+ Application spyApplication = spy(RuntimeEnvironment.application);
+ when(spyApplication.getContentResolver()).thenReturn(mockResolver);
+
+ ExportOperation op = new ExportOperation(spyApplication,
+ new ProviderHelper(RuntimeEnvironment.application), null);
+
+ ExportKeyringParcel parcel = new ExportKeyringParcel(null,
+ new long[] { mStaticRing1.getMasterKeyId() }, false, fakeOutputUri);
+
+ ExportResult result = op.execute(parcel, null);
+
+ verify(mockResolver).openOutputStream(fakeOutputUri);
+
+ assertTrue("export must succeed", result.success());
+
+ TestingUtils.assertArrayEqualsPrefix("exported data must start with ascii armor header",
+ "-----BEGIN PGP PUBLIC KEY BLOCK-----\n".getBytes(), outStream1.toByteArray());
+ TestingUtils.assertArrayEqualsSuffix("exported data must end with ascii armor header",
+ "-----END PGP PUBLIC KEY BLOCK-----\n".getBytes(), outStream1.toByteArray());
+
+ {
+ IteratorWithIOThrow<UncachedKeyRing> unc
+ = UncachedKeyRing.fromStream(new ByteArrayInputStream(outStream1.toByteArray()));
+
+ assertTrue("export must have one key", unc.hasNext());
+ UncachedKeyRing ring = unc.next();
+ Assert.assertEquals("exported key has correct masterkeyid",
+ mStaticRing1.getMasterKeyId(), ring.getMasterKeyId());
+ assertFalse("export must have exactly one key", unc.hasNext());
+ }
+
+ }
+
+ @Test
+ public void testExportEncrypted() throws Exception {
+
+
+ Application spyApplication;
+ ContentResolver mockResolver = mock(ContentResolver.class);
+
+ Uri fakePipedUri, fakeOutputUri;
+ ByteArrayOutputStream outStream; {
+
+ fakePipedUri = Uri.parse("content://fake/pipe/1");
+ PipedInputStream pipedInStream = new PipedInputStream(8192);
+ PipedOutputStream pipedOutStream = new PipedOutputStream(pipedInStream);
+ when(mockResolver.openOutputStream(fakePipedUri)).thenReturn(pipedOutStream);
+ when(mockResolver.openInputStream(fakePipedUri)).thenReturn(pipedInStream);
+ when(mockResolver.insert(eq(TemporaryStorageProvider.CONTENT_URI), any(ContentValues.class)))
+ .thenReturn(fakePipedUri);
+
+ fakeOutputUri = Uri.parse("content://fake/out/1");
+ outStream = new ByteArrayOutputStream();
+ when(mockResolver.openOutputStream(fakeOutputUri)).thenReturn(outStream);
+
+ spyApplication = spy(RuntimeEnvironment.application);
+ when(spyApplication.getContentResolver()).thenReturn(mockResolver);
+ }
+
+ Passphrase passphrase = new Passphrase("abcde");
+
+ { // export encrypted
+ ExportOperation op = new ExportOperation(spyApplication,
+ new ProviderHelper(RuntimeEnvironment.application), null);
+
+ ExportKeyringParcel parcel = new ExportKeyringParcel(passphrase,
+ new long[] { mStaticRing1.getMasterKeyId() }, false, fakeOutputUri);
+
+ ExportResult result = op.execute(parcel, null);
+
+ verify(mockResolver).openOutputStream(fakePipedUri);
+ verify(mockResolver).openInputStream(fakePipedUri);
+ verify(mockResolver).openOutputStream(fakeOutputUri);
+
+ assertTrue("export must succeed", result.success());
+ TestingUtils.assertArrayEqualsPrefix("exported data must start with ascii armor header",
+ "-----BEGIN PGP MESSAGE-----\n".getBytes(), outStream.toByteArray());
+ }
+
+ {
+ PgpDecryptVerifyOperation op = new PgpDecryptVerifyOperation(RuntimeEnvironment.application,
+ new ProviderHelper(RuntimeEnvironment.application), null);
+
+ PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel(outStream.toByteArray());
+ input.setAllowSymmetricDecryption(true);
+
+ {
+ DecryptVerifyResult result = op.execute(input, new CryptoInputParcel());
+ assertTrue("decryption must return pending without passphrase", result.isPending());
+ Assert.assertTrue("should contain pending passphrase log entry",
+ result.getLog().containsType(LogType.MSG_DC_PENDING_PASSPHRASE));
+ }
+ {
+ DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(new Passphrase("bad")));
+ assertFalse("decryption must fail with bad passphrase", result.success());
+ Assert.assertTrue("should contain bad passphrase log entry",
+ result.getLog().containsType(LogType.MSG_DC_ERROR_SYM_PASSPHRASE));
+ }
+
+ DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(passphrase));
+ assertTrue("decryption must succeed with passphrase", result.success());
+
+ assertEquals("backup filename should be backup_keyid.pub.asc",
+ "backup_" + KeyFormattingUtils.convertKeyIdToHex(mStaticRing1.getMasterKeyId()) + ".pub.asc",
+ result.getDecryptionMetadata().getFilename());
+
+ assertEquals("mime type for pgp keys must be correctly detected",
+ "application/pgp-keys", result.getDecryptionMetadata().getMimeType());
+
+ TestingUtils.assertArrayEqualsPrefix("exported data must start with ascii armor header",
+ "-----BEGIN PGP PUBLIC KEY BLOCK-----\n".getBytes(), result.getOutputBytes());
+ TestingUtils.assertArrayEqualsSuffix("exported data must end with ascii armor header",
+ "-----END PGP PUBLIC KEY BLOCK-----\n".getBytes(), result.getOutputBytes());
+
+ {
+ IteratorWithIOThrow<UncachedKeyRing> unc
+ = UncachedKeyRing.fromStream(new ByteArrayInputStream(result.getOutputBytes()));
+
+ assertTrue("export must have one key", unc.hasNext());
+ UncachedKeyRing ring = unc.next();
+ Assert.assertEquals("exported key has correct masterkeyid",
+ mStaticRing1.getMasterKeyId(), ring.getMasterKeyId());
+ assertFalse("export must have exactly one key", unc.hasNext());
+ }
+
+ }
+
+ }
+
+
/** This function checks whether or not there are any local signatures in a keyring. */
private boolean checkForLocal(UncachedKeyRing ring) {
Iterator<WrappedSignature> sigs = ring.getPublicKey().getSignatures();
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/TestingUtils.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/TestingUtils.java
index 1d7952464..0b35aaf22 100644
--- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/TestingUtils.java
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/TestingUtils.java
@@ -19,6 +19,9 @@ package org.sufficientlysecure.keychain.util;
import java.util.Random;
+import junit.framework.Assert;
+
+
public class TestingUtils {
public static Passphrase genPassphrase() {
return genPassphrase(false);
@@ -35,4 +38,25 @@ public class TestingUtils {
System.out.println("Generated passphrase: '" + passbuilder.toString() + "'");
return new Passphrase(passbuilder.toString());
}
+
+ public static void assertArrayEqualsPrefix(String msg, byte[] expected, byte[] actual) {
+
+ Assert.assertTrue("exepected must be shorter or equal to actual array length",
+ expected.length <= actual.length);
+ for (int i = 0; i < expected.length; i++) {
+ Assert.assertEquals(msg, expected[i], actual[i]);
+ }
+
+ }
+
+ public static void assertArrayEqualsSuffix(String msg, byte[] expected, byte[] actual) {
+
+ Assert.assertTrue("exepected must be shorter or equal to actual array length",
+ expected.length <= actual.length);
+ for (int i = 0; i < expected.length; i++) {
+ Assert.assertEquals(msg, expected[i], actual[actual.length -expected.length +i]);
+ }
+
+ }
+
}