Merge pull request #1487 from open-keychain/mime4j

support multipart mime structure in decrypted data
This commit is contained in:
Vincent
2015-09-18 14:11:49 +02:00
53 changed files with 1313 additions and 332 deletions

View File

@@ -27,6 +27,7 @@ python copy OpenKeychain communication grey import_export 24
python copy OpenKeychain content grey content_copy 24 python copy OpenKeychain content grey content_copy 24
python copy OpenKeychain content grey content_paste 24 python copy OpenKeychain content grey content_paste 24
python copy OpenKeychain content grey save 24 python copy OpenKeychain content grey save 24
python copy OpenKeychain content black save 24
python copy OpenKeychain content grey select_all 24 python copy OpenKeychain content grey select_all 24
python copy OpenKeychain editor grey mode_edit 24 python copy OpenKeychain editor grey mode_edit 24
python copy OpenKeychain file grey cloud 24 python copy OpenKeychain file grey cloud 24
@@ -37,6 +38,7 @@ python copy OpenKeychain navigation grey close 24
python copy OpenKeychain social grey person 24 python copy OpenKeychain social grey person 24
python copy OpenKeychain social grey person_add 24 python copy OpenKeychain social grey person_add 24
python copy OpenKeychain social grey share 24 python copy OpenKeychain social grey share 24
python copy OpenKeychain social white share 24
python copy OpenKeychain communication grey vpn_key 24 python copy OpenKeychain communication grey vpn_key 24
python copy OpenKeychain navigation grey chevron_left 24 python copy OpenKeychain navigation grey chevron_left 24
python copy OpenKeychain navigation grey chevron_right 24 python copy OpenKeychain navigation grey chevron_right 24
@@ -44,6 +46,8 @@ python copy OpenKeychain social grey person 48
python copy OpenKeychain communication grey email 24 python copy OpenKeychain communication grey email 24
python copy OpenKeychain social black share 24 python copy OpenKeychain social black share 24
python copy OpenKeychain content black content_copy 24 python copy OpenKeychain content black content_copy 24
python copy OpenKeychain communication black chat 24
python copy OpenKeychain navigation black more_vert 24
# navigation drawer sections # navigation drawer sections
python copy OpenKeychain communication black vpn_key 24 python copy OpenKeychain communication black vpn_key 24

View File

@@ -20,6 +20,7 @@ dependencies {
// http://www.vogella.com/tutorials/Robolectric/article.html // http://www.vogella.com/tutorials/Robolectric/article.html
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'
testCompile 'org.robolectric:robolectric:3.0' testCompile 'org.robolectric:robolectric:3.0'
testCompile 'org.mockito:mockito-core:1.+'
// UI testing with Espresso // UI testing with Espresso
androidTestCompile 'com.android.support.test:runner:0.3' androidTestCompile 'com.android.support.test:runner:0.3'
@@ -57,7 +58,10 @@ dependencies {
compile 'com.mikepenz.iconics:community-material-typeface:1.0.0@aar' compile 'com.mikepenz.iconics:community-material-typeface:1.0.0@aar'
compile 'com.nispok:snackbar:2.11.0' compile 'com.nispok:snackbar:2.11.0'
compile 'com.squareup.okhttp:okhttp:2.4.0' compile 'com.squareup.okhttp:okhttp:2.4.0'
compile 'org.apache.james:apache-mime4j-core:0.7.2'
compile 'org.apache.james:apache-mime4j-dom:0.7.2'
compile 'org.thoughtcrime.ssl.pinning:AndroidPinning:1.0.0' compile 'org.thoughtcrime.ssl.pinning:AndroidPinning:1.0.0'
compile 'com.cocosw:bottomsheet:1.1.1@aar'
// libs as submodules // libs as submodules
compile project(':extern:openpgp-api-lib:openpgp-api') compile project(':extern:openpgp-api-lib:openpgp-api')
@@ -209,9 +213,9 @@ android {
htmlOutput file('lint-report.html') htmlOutput file('lint-report.html')
} }
// Disable preDexing, causes com.android.dx.cf.iface.ParseException: bad class file magic (cafebabe) or version (0034.0000) on some systems
dexOptions { dexOptions {
incremental = true incremental = true
// Disable preDexing, causes com.android.dx.cf.iface.ParseException: bad class file magic (cafebabe) or version (0034.0000) on some systems
preDexLibraries = false preDexLibraries = false
jumboMode = true jumboMode = true
javaMaxHeapSize "2g" javaMaxHeapSize "2g"
@@ -221,6 +225,9 @@ android {
exclude 'LICENSE.txt' exclude 'LICENSE.txt'
exclude 'META-INF/LICENSE.txt' exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/NOTICE.txt' exclude 'META-INF/NOTICE.txt'
exclude 'META-INF/DEPENDENCIES'
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE'
exclude '.readme' exclude '.readme'
} }
} }

View File

@@ -133,7 +133,7 @@ public class SymmetricTextOperationTests {
hasExtra(equalTo(Intent.EXTRA_INTENT), allOf( hasExtra(equalTo(Intent.EXTRA_INTENT), allOf(
hasAction(Intent.ACTION_VIEW), hasAction(Intent.ACTION_VIEW),
hasFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION), hasFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION),
hasData(allOf(hasScheme("content"), hasHost(TemporaryStorageProvider.CONTENT_AUTHORITY))), hasData(allOf(hasScheme("content"), hasHost(TemporaryStorageProvider.AUTHORITY))),
hasType("text/plain") hasType("text/plain")
)) ))
)).respondWith(new ActivityResult(Activity.RESULT_OK, null)); )).respondWith(new ActivityResult(Activity.RESULT_OK, null));

View File

@@ -96,7 +96,7 @@ public class ViewKeyAdvShareTest {
hasType("text/plain"), hasType("text/plain"),
hasExtra(is(Intent.EXTRA_TEXT), is("openpgp4fpr:c619d53f7a5f96f391a84ca79d604d2f310716a3")), hasExtra(is(Intent.EXTRA_TEXT), is("openpgp4fpr:c619d53f7a5f96f391a84ca79d604d2f310716a3")),
hasExtra(is(Intent.EXTRA_STREAM), hasExtra(is(Intent.EXTRA_STREAM),
allOf(hasScheme("content"), hasHost(TemporaryStorageProvider.CONTENT_AUTHORITY))) allOf(hasScheme("content"), hasHost(TemporaryStorageProvider.AUTHORITY)))
)) ))
)).respondWith(new ActivityResult(Activity.RESULT_OK, null)); )).respondWith(new ActivityResult(Activity.RESULT_OK, null));
onView(withId(R.id.view_key_action_fingerprint_share)).perform(click()); onView(withId(R.id.view_key_action_fingerprint_share)).perform(click());
@@ -113,7 +113,7 @@ public class ViewKeyAdvShareTest {
hasType("text/plain"), hasType("text/plain"),
hasExtra(is(Intent.EXTRA_TEXT), startsWith("----")), hasExtra(is(Intent.EXTRA_TEXT), startsWith("----")),
hasExtra(is(Intent.EXTRA_STREAM), hasExtra(is(Intent.EXTRA_STREAM),
allOf(hasScheme("content"), hasHost(TemporaryStorageProvider.CONTENT_AUTHORITY))) allOf(hasScheme("content"), hasHost(TemporaryStorageProvider.AUTHORITY)))
)) ))
)).respondWith(new ActivityResult(Activity.RESULT_OK, null)); )).respondWith(new ActivityResult(Activity.RESULT_OK, null));
onView(withId(R.id.view_key_action_key_share)).perform(click()); onView(withId(R.id.view_key_action_key_share)).perform(click());

View File

@@ -256,7 +256,7 @@
This links to attached asc files in AOSP mail. It is deactivated because of This links to attached asc files in AOSP mail. It is deactivated because of
https://github.com/open-keychain/open-keychain/issues/290 https://github.com/open-keychain/open-keychain/issues/290
--> -->
<!--<data android:mimeType="text/plain" />--> <data android:mimeType="text/plain" />
</intent-filter> </intent-filter>
<!-- DECRYPT_DATA with data Uri --> <!-- DECRYPT_DATA with data Uri -->
<intent-filter> <intent-filter>

View File

@@ -0,0 +1,374 @@
/*
* Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.operations;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import org.apache.james.mime4j.MimeException;
import org.apache.james.mime4j.codec.DecodeMonitor;
import org.apache.james.mime4j.dom.field.ContentDispositionField;
import org.apache.james.mime4j.field.DefaultFieldParser;
import org.apache.james.mime4j.parser.AbstractContentHandler;
import org.apache.james.mime4j.parser.MimeStreamParser;
import org.apache.james.mime4j.stream.BodyDescriptor;
import org.apache.james.mime4j.stream.Field;
import org.apache.james.mime4j.stream.MimeConfig;
import org.openintents.openpgp.OpenPgpMetadata;
import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult;
import org.sufficientlysecure.keychain.operations.results.InputDataResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel;
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation;
import org.sufficientlysecure.keychain.pgp.Progressable;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider;
import org.sufficientlysecure.keychain.service.InputDataParcel;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
/** This operation deals with input data, trying to determine its type as it goes.
*
* We deal with four types of structures:
*
* - signed/encrypted non-mime data
* - signed/encrypted mime data
* - encrypted multipart/signed mime data
* - multipart/signed mime data (WIP)
*
*/
public class InputDataOperation extends BaseOperation<InputDataParcel> {
final private byte[] buf = new byte[256];
public InputDataOperation(Context context, ProviderHelper providerHelper, Progressable progressable) {
super(context, providerHelper, progressable);
}
Uri mSignedDataUri;
DecryptVerifyResult mSignedDataResult;
@NonNull
@Override
public InputDataResult execute(InputDataParcel input, final CryptoInputParcel cryptoInput) {
final OperationLog log = new OperationLog();
log.add(LogType.MSG_DATA, 0);
Uri currentInputUri;
DecryptVerifyResult decryptResult = null;
PgpDecryptVerifyInputParcel decryptInput = input.getDecryptInput();
if (decryptInput != null) {
log.add(LogType.MSG_DATA_OPENPGP, 1);
PgpDecryptVerifyOperation op =
new PgpDecryptVerifyOperation(mContext, mProviderHelper, mProgressable);
decryptInput.setInputUri(input.getInputUri());
currentInputUri = TemporaryStorageProvider.createFile(mContext);
decryptInput.setOutputUri(currentInputUri);
decryptResult = op.execute(decryptInput, cryptoInput);
if (decryptResult.isPending()) {
return new InputDataResult(log, decryptResult);
}
log.addByMerge(decryptResult, 2);
if (!decryptResult.success()) {
log.add(LogType.MSG_DATA_ERROR_OPENPGP, 1);
return new InputDataResult(InputDataResult.RESULT_ERROR, log);
}
} else {
currentInputUri = input.getInputUri();
}
// If we aren't supposed to attempt mime decode, we are done here
if (!input.getMimeDecode()) {
if (decryptInput == null) {
throw new AssertionError("no decryption or mime decoding, this is probably a bug");
}
log.add(LogType.MSG_DATA_SKIP_MIME, 1);
ArrayList<Uri> uris = new ArrayList<>();
uris.add(currentInputUri);
ArrayList<OpenPgpMetadata> metadatas = new ArrayList<>();
metadatas.add(decryptResult.getDecryptionMetadata());
log.add(LogType.MSG_DATA_OK, 1);
return new InputDataResult(InputDataResult.RESULT_OK, log, decryptResult, uris, metadatas);
}
final MimeStreamParser parser = new MimeStreamParser((MimeConfig) null);
final ArrayList<Uri> outputUris = new ArrayList<>();
final ArrayList<OpenPgpMetadata> metadatas = new ArrayList<>();
parser.setContentDecoding(true);
parser.setRecurse();
parser.setContentHandler(new AbstractContentHandler() {
private Uri uncheckedSignedDataUri;
String mFilename;
@Override
public void startMultipart(BodyDescriptor bd) throws MimeException {
if ("signed".equals(bd.getSubType())) {
if (mSignedDataUri != null) {
// recursive signed data is not supported, and will just be parsed as-is
log.add(LogType.MSG_DATA_DETACHED_NESTED, 2);
return;
}
log.add(LogType.MSG_DATA_DETACHED, 2);
if (!outputUris.isEmpty()) {
// we can't have previous data if we parse a detached signature!
log.add(LogType.MSG_DATA_DETACHED_CLEAR, 3);
outputUris.clear();
metadatas.clear();
}
// this is signed data, we require the next part raw
parser.setRaw();
}
}
@Override
public void raw(InputStream is) throws MimeException, IOException {
if (uncheckedSignedDataUri != null) {
throw new AssertionError("raw parts must only be received as first part of multipart/signed!");
}
log.add(LogType.MSG_DATA_DETACHED_RAW, 3);
uncheckedSignedDataUri = TemporaryStorageProvider.createFile(mContext, mFilename, "text/plain");
OutputStream out = mContext.getContentResolver().openOutputStream(uncheckedSignedDataUri, "w");
if (out == null) {
throw new IOException("Error getting file for writing!");
}
int len;
while ((len = is.read(buf)) > 0) {
out.write(buf, 0, len);
}
out.close();
// continue to next body part the usual way
parser.setFlat();
}
@Override
public void startHeader() throws MimeException {
mFilename = null;
}
@Override
public void field(Field field) throws MimeException {
field = DefaultFieldParser.getParser().parse(field, DecodeMonitor.SILENT);
if (field instanceof ContentDispositionField) {
mFilename = ((ContentDispositionField) field).getFilename();
}
}
private void bodySignature(BodyDescriptor bd, InputStream is) throws MimeException, IOException {
if (!"application/pgp-signature".equals(bd.getMimeType())) {
log.add(LogType.MSG_DATA_DETACHED_UNSUPPORTED, 3);
uncheckedSignedDataUri = null;
parser.setRecurse();
return;
}
log.add(LogType.MSG_DATA_DETACHED_SIG, 3);
ByteArrayOutputStream detachedSig = new ByteArrayOutputStream();
int len, totalLength = 0;
while ((len = is.read(buf)) > 0) {
totalLength += len;
detachedSig.write(buf, 0, len);
if (totalLength > 4096) {
throw new IOException("detached signature is unreasonably large!");
}
}
detachedSig.close();
PgpDecryptVerifyInputParcel decryptInput = new PgpDecryptVerifyInputParcel();
decryptInput.setInputUri(uncheckedSignedDataUri);
decryptInput.setDetachedSignature(detachedSig.toByteArray());
PgpDecryptVerifyOperation op =
new PgpDecryptVerifyOperation(mContext, mProviderHelper, mProgressable);
DecryptVerifyResult verifyResult = op.execute(decryptInput, cryptoInput);
log.addByMerge(verifyResult, 4);
mSignedDataUri = uncheckedSignedDataUri;
mSignedDataResult = verifyResult;
// reset parser state
uncheckedSignedDataUri = null;
parser.setRecurse();
}
@Override
public void body(BodyDescriptor bd, InputStream is) throws MimeException, IOException {
// if we have signed data waiting, we expect a signature for checking
if (uncheckedSignedDataUri != null) {
bodySignature(bd, is);
return;
}
// we read first, no need to create an output file if nothing was read!
int len = is.read(buf);
if (len < 0) {
return;
}
// If mSignedDataUri is non-null, we already parsed a signature. If mSignedDataResult is non-null
// too, we are still in the same parsing stage, so this is trailing data - skip it!
if (mSignedDataUri != null && mSignedDataResult != null) {
log.add(LogType.MSG_DATA_DETACHED_TRAILING, 2);
return;
}
log.add(LogType.MSG_DATA_MIME_PART, 2);
log.add(LogType.MSG_DATA_MIME_TYPE, 3, bd.getMimeType());
if (mFilename != null) {
log.add(LogType.MSG_DATA_MIME_FILENAME, 3, mFilename);
}
Uri uri = TemporaryStorageProvider.createFile(mContext, mFilename, bd.getMimeType());
OutputStream out = mContext.getContentResolver().openOutputStream(uri, "w");
if (out == null) {
throw new IOException("Error getting file for writing!");
}
int totalLength = 0;
do {
totalLength += len;
out.write(buf, 0, len);
} while ((len = is.read(buf)) > 0);
log.add(LogType.MSG_DATA_MIME_LENGTH, 3, Long.toString(totalLength));
String charset = bd.getCharset();
// the charset defaults to us-ascii, but we want to default to utf-8
if ("us-ascii".equals(charset)) {
charset = "utf-8";
}
OpenPgpMetadata metadata = new OpenPgpMetadata(mFilename, bd.getMimeType(), 0L, totalLength, charset);
out.close();
outputUris.add(uri);
metadatas.add(metadata);
}
});
try {
log.add(LogType.MSG_DATA_MIME, 1);
// open current uri for input
InputStream in = mContext.getContentResolver().openInputStream(currentInputUri);
parser.parse(in);
if (mSignedDataUri != null) {
if (decryptResult != null) {
decryptResult.setSignatureResult(mSignedDataResult.getSignatureResult());
} else {
decryptResult = mSignedDataResult;
}
// the actual content is the signed data now (and will be passed verbatim, if parsing fails)
currentInputUri = mSignedDataUri;
in = mContext.getContentResolver().openInputStream(currentInputUri);
// reset signed data result, to indicate to the parser that it is in the inner part
mSignedDataResult = null;
parser.parse(in);
}
// if we found data, return success
if (!outputUris.isEmpty()) {
log.add(LogType.MSG_DATA_MIME_OK, 2);
log.add(LogType.MSG_DATA_OK, 1);
return new InputDataResult(InputDataResult.RESULT_OK, log, decryptResult, outputUris, metadatas);
}
// if no mime data parsed, just return the raw data as fallback
log.add(LogType.MSG_DATA_MIME_NONE, 2);
OpenPgpMetadata metadata;
if (decryptResult != null) {
metadata = decryptResult.getDecryptionMetadata();
} else {
// if we neither decrypted nor mime-decoded, should this be treated as an error?
// either way, we know nothing about the data
metadata = new OpenPgpMetadata();
}
outputUris.add(currentInputUri);
metadatas.add(metadata);
log.add(LogType.MSG_DATA_OK, 1);
return new InputDataResult(InputDataResult.RESULT_OK, log, decryptResult, outputUris, metadatas);
} catch (FileNotFoundException e) {
log.add(LogType.MSG_DATA_ERROR_IO, 2);
return new InputDataResult(InputDataResult.RESULT_ERROR, log);
} catch (IOException e) {
e.printStackTrace();
log.add(LogType.MSG_DATA_ERROR_IO, 2);
return new InputDataResult(InputDataResult.RESULT_ERROR, log);
} catch (MimeException e) {
e.printStackTrace();
log.add(LogType.MSG_DATA_MIME_ERROR, 2);
return new InputDataResult(InputDataResult.RESULT_ERROR, log);
}
}
}

View File

@@ -34,9 +34,6 @@ public class DecryptVerifyResult extends InputPendingResult {
OpenPgpSignatureResult mSignatureResult; OpenPgpSignatureResult mSignatureResult;
OpenPgpDecryptionResult mDecryptionResult; OpenPgpDecryptionResult mDecryptionResult;
OpenPgpMetadata mDecryptionMetadata; OpenPgpMetadata mDecryptionMetadata;
// This holds the charset which was specified in the ascii armor, if specified
// https://tools.ietf.org/html/rfc4880#page56
String mCharset;
CryptoInputParcel mCachedCryptoInputParcel; CryptoInputParcel mCachedCryptoInputParcel;
@@ -96,14 +93,6 @@ public class DecryptVerifyResult extends InputPendingResult {
mDecryptionMetadata = decryptMetadata; mDecryptionMetadata = decryptMetadata;
} }
public String getCharset () {
return mCharset;
}
public void setCharset(String charset) {
mCharset = charset;
}
public void setOutputBytes(byte[] outputBytes) { public void setOutputBytes(byte[] outputBytes) {
mOutputBytes = outputBytes; mOutputBytes = outputBytes;
} }

View File

@@ -0,0 +1,96 @@
/*
* Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.operations.results;
import java.util.ArrayList;
import android.net.Uri;
import android.os.Parcel;
import android.support.annotation.NonNull;
import org.openintents.openpgp.OpenPgpMetadata;
public class InputDataResult extends InputPendingResult {
public final ArrayList<Uri> mOutputUris;
final public DecryptVerifyResult mDecryptVerifyResult;
public final ArrayList<OpenPgpMetadata> mMetadata;
public InputDataResult(OperationLog log, @NonNull InputPendingResult result) {
super(log, result);
mOutputUris = null;
mDecryptVerifyResult = null;
mMetadata = null;
}
public InputDataResult(int result, OperationLog log) {
super(result, log);
mOutputUris = null;
mDecryptVerifyResult = null;
mMetadata = null;
}
public InputDataResult(int result, OperationLog log, DecryptVerifyResult decryptResult,
@NonNull ArrayList<Uri> outputUris, @NonNull ArrayList<OpenPgpMetadata> metadata) {
super(result, log);
mDecryptVerifyResult = decryptResult;
if (outputUris.size() != metadata.size()) {
throw new AssertionError("number of output URIs must match metadata!");
}
mOutputUris = outputUris;
mMetadata = metadata;
}
protected InputDataResult(Parcel in) {
super(in);
mOutputUris = in.createTypedArrayList(Uri.CREATOR);
mDecryptVerifyResult = in.readParcelable(DecryptVerifyResult.class.getClassLoader());
mMetadata = in.createTypedArrayList(OpenPgpMetadata.CREATOR);
}
public ArrayList<Uri> getOutputUris() {
return mOutputUris;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeTypedList(mOutputUris);
dest.writeParcelable(mDecryptVerifyResult, 0);
dest.writeTypedList(mMetadata);
}
public static final Creator<InputDataResult> CREATOR = new Creator<InputDataResult>() {
@Override
public InputDataResult createFromParcel(Parcel in) {
return new InputDataResult(in);
}
@Override
public InputDataResult[] newArray(int size) {
return new InputDataResult[size];
}
};
}

View File

@@ -38,6 +38,15 @@ public class InputPendingResult extends OperationResult {
mCryptoInputParcel = null; mCryptoInputParcel = null;
} }
public InputPendingResult(OperationLog log, InputPendingResult result) {
super(RESULT_PENDING, log);
if (!result.isPending()) {
throw new AssertionError("sub result must be pending!");
}
mRequiredInput = result.mRequiredInput;
mCryptoInputParcel = result.mCryptoInputParcel;
}
public InputPendingResult(OperationLog log, RequiredInputParcel requiredInput, public InputPendingResult(OperationLog log, RequiredInputParcel requiredInput,
CryptoInputParcel cryptoInputParcel) { CryptoInputParcel cryptoInputParcel) {
super(RESULT_PENDING, log); super(RESULT_PENDING, log);

View File

@@ -126,6 +126,13 @@ public abstract class OperationResult implements Parcelable {
Log.v(Constants.TAG, "log: " + this); Log.v(Constants.TAG, "log: " + this);
} }
/** Clones this LogEntryParcel, adding extra indent. Note that the parameter array is NOT cloned! */
public LogEntryParcel (LogEntryParcel original, int extraIndent) {
mType = original.mType;
mParameters = original.mParameters;
mIndent = original.mIndent +extraIndent;
}
public LogEntryParcel(Parcel source) { public LogEntryParcel(Parcel source) {
mType = LogType.values()[source.readInt()]; mType = LogType.values()[source.readInt()];
mParameters = (Object[]) source.readSerializable(); mParameters = (Object[]) source.readSerializable();
@@ -818,7 +825,29 @@ public abstract class OperationResult implements Parcelable {
MSG_KEYBASE_ERROR_PAYLOAD_MISMATCH(LogLevel.ERROR, MSG_KEYBASE_ERROR_PAYLOAD_MISMATCH(LogLevel.ERROR,
R.string.msg_keybase_error_msg_payload_mismatch), R.string.msg_keybase_error_msg_payload_mismatch),
// export log // InputData Operation
MSG_DATA (LogLevel.START, R.string.msg_data),
MSG_DATA_OPENPGP (LogLevel.DEBUG, R.string.msg_data_openpgp),
MSG_DATA_ERROR_IO (LogLevel.ERROR, R.string.msg_data_error_io),
MSG_DATA_ERROR_OPENPGP (LogLevel.ERROR, R.string.msg_data_error_openpgp),
MSG_DATA_DETACHED (LogLevel.INFO, R.string.msg_data_detached),
MSG_DATA_DETACHED_CLEAR (LogLevel.WARN, R.string.msg_data_detached_clear),
MSG_DATA_DETACHED_SIG (LogLevel.DEBUG, R.string.msg_data_detached_sig),
MSG_DATA_DETACHED_RAW (LogLevel.DEBUG, R.string.msg_data_detached_raw),
MSG_DATA_DETACHED_NESTED(LogLevel.WARN, R.string.msg_data_detached_nested),
MSG_DATA_DETACHED_TRAILING (LogLevel.WARN, R.string.msg_data_detached_trailing),
MSG_DATA_DETACHED_UNSUPPORTED (LogLevel.WARN, R.string.msg_data_detached_unsupported),
MSG_DATA_MIME_ERROR (LogLevel.ERROR, R.string.msg_data_mime_error),
MSG_DATA_MIME_FILENAME (LogLevel.DEBUG, R.string.msg_data_mime_filename),
MSG_DATA_MIME_LENGTH (LogLevel.DEBUG, R.string.msg_data_mime_length),
MSG_DATA_MIME (LogLevel.DEBUG, R.string.msg_data_mime),
MSG_DATA_MIME_OK (LogLevel.INFO, R.string.msg_data_mime_ok),
MSG_DATA_MIME_NONE (LogLevel.DEBUG, R.string.msg_data_mime_none),
MSG_DATA_MIME_PART (LogLevel.DEBUG, R.string.msg_data_mime_part),
MSG_DATA_MIME_TYPE (LogLevel.DEBUG, R.string.msg_data_mime_type),
MSG_DATA_OK (LogLevel.OK, R.string.msg_data_ok),
MSG_DATA_SKIP_MIME (LogLevel.DEBUG, R.string.msg_data_skip_mime),
MSG_LV (LogLevel.START, R.string.msg_lv), MSG_LV (LogLevel.START, R.string.msg_lv),
MSG_LV_MATCH (LogLevel.DEBUG, R.string.msg_lv_match), MSG_LV_MATCH (LogLevel.DEBUG, R.string.msg_lv_match),
MSG_LV_MATCH_ERROR (LogLevel.ERROR, R.string.msg_lv_match_error), MSG_LV_MATCH_ERROR (LogLevel.ERROR, R.string.msg_lv_match_error),
@@ -838,7 +867,8 @@ public abstract class OperationResult implements Parcelable {
MSG_LV_FETCH_ERROR_URL (LogLevel.ERROR, R.string.msg_lv_fetch_error_url), MSG_LV_FETCH_ERROR_URL (LogLevel.ERROR, R.string.msg_lv_fetch_error_url),
MSG_LV_FETCH_ERROR_IO (LogLevel.ERROR, R.string.msg_lv_fetch_error_io), MSG_LV_FETCH_ERROR_IO (LogLevel.ERROR, R.string.msg_lv_fetch_error_io),
MSG_LV_FETCH_ERROR_FORMAT(LogLevel.ERROR, R.string.msg_lv_fetch_error_format), MSG_LV_FETCH_ERROR_FORMAT(LogLevel.ERROR, R.string.msg_lv_fetch_error_format),
MSG_LV_FETCH_ERROR_NOTHING (LogLevel.ERROR, R.string.msg_lv_fetch_error_nothing); MSG_LV_FETCH_ERROR_NOTHING (LogLevel.ERROR, R.string.msg_lv_fetch_error_nothing),
;
public final int mMsgId; public final int mMsgId;
public final LogLevel mLevel; public final LogLevel mLevel;
@@ -896,6 +926,13 @@ public abstract class OperationResult implements Parcelable {
mParcels.add(new SubLogEntryParcel(subResult, subLog.getFirst().mType, indent, subLog.getFirst().mParameters)); mParcels.add(new SubLogEntryParcel(subResult, subLog.getFirst().mType, indent, subLog.getFirst().mParameters));
} }
public void addByMerge(OperationResult subResult, int indent) {
OperationLog subLog = subResult.getLog();
for (LogEntryParcel entry : subLog) {
mParcels.add(new LogEntryParcel(entry, indent));
}
}
public SubLogEntryParcel getSubResultIfSingle() { public SubLogEntryParcel getSubResultIfSingle() {
if (mParcels.size() != 1) { if (mParcels.size() != 1) {
return null; return null;

View File

@@ -18,6 +18,7 @@
package org.sufficientlysecure.keychain.pgp; package org.sufficientlysecure.keychain.pgp;
import java.io.InputStream;
import java.util.HashSet; import java.util.HashSet;
import android.net.Uri; import android.net.Uri;
@@ -86,10 +87,20 @@ public class PgpDecryptVerifyInputParcel implements Parcelable {
return mInputBytes; return mInputBytes;
} }
public PgpDecryptVerifyInputParcel setInputUri(Uri uri) {
mInputUri = uri;
return this;
}
Uri getInputUri() { Uri getInputUri() {
return mInputUri; return mInputUri;
} }
public PgpDecryptVerifyInputParcel setOutputUri(Uri uri) {
mOutputUri = uri;
return this;
}
Uri getOutputUri() { Uri getOutputUri() {
return mOutputUri; return mOutputUri;
} }

View File

@@ -556,12 +556,12 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
originalFilename, originalFilename,
mimeType, mimeType,
literalData.getModificationTime().getTime(), literalData.getModificationTime().getTime(),
originalSize == null ? 0 : originalSize); originalSize == null ? 0 : originalSize,
charset);
log.add(LogType.MSG_DC_OK_META_ONLY, indent); log.add(LogType.MSG_DC_OK_META_ONLY, indent);
DecryptVerifyResult result = DecryptVerifyResult result =
new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log); new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log);
result.setCharset(charset);
result.setDecryptionMetadata(metadata); result.setDecryptionMetadata(metadata);
return result; return result;
} }
@@ -607,7 +607,7 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
} }
metadata = new OpenPgpMetadata( metadata = new OpenPgpMetadata(
originalFilename, mimeType, literalData.getModificationTime().getTime(), alreadyWritten); originalFilename, mimeType, literalData.getModificationTime().getTime(), alreadyWritten, charset);
if (signature != null) { if (signature != null) {
updateProgress(R.string.progress_verifying_signature, 90, 100); updateProgress(R.string.progress_verifying_signature, 90, 100);
@@ -663,7 +663,6 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
result.setCachedCryptoInputParcel(cryptoInput); result.setCachedCryptoInputParcel(cryptoInput);
result.setSignatureResult(signatureResultBuilder.build()); result.setSignatureResult(signatureResultBuilder.build());
result.setCharset(charset);
result.setDecryptionResult(decryptionResultBuilder.build()); result.setDecryptionResult(decryptionResultBuilder.build());
result.setDecryptionMetadata(metadata); result.setDecryptionMetadata(metadata);

View File

@@ -67,8 +67,8 @@ public class TemporaryStorageProvider extends ContentProvider {
private static final String COLUMN_NAME = "name"; private static final String COLUMN_NAME = "name";
private static final String COLUMN_TIME = "time"; private static final String COLUMN_TIME = "time";
private static final String COLUMN_TYPE = "mimetype"; private static final String COLUMN_TYPE = "mimetype";
public static final String CONTENT_AUTHORITY = Constants.TEMPSTORAGE_AUTHORITY; public static final String AUTHORITY = Constants.TEMPSTORAGE_AUTHORITY;
private static final Uri BASE_URI = Uri.parse("content://" + CONTENT_AUTHORITY); public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY);
private static final int DB_VERSION = 3; private static final int DB_VERSION = 3;
private static File cacheDir; private static File cacheDir;
@@ -77,18 +77,18 @@ public class TemporaryStorageProvider extends ContentProvider {
ContentValues contentValues = new ContentValues(); ContentValues contentValues = new ContentValues();
contentValues.put(COLUMN_NAME, targetName); contentValues.put(COLUMN_NAME, targetName);
contentValues.put(COLUMN_TYPE, mimeType); contentValues.put(COLUMN_TYPE, mimeType);
return context.getContentResolver().insert(BASE_URI, contentValues); return context.getContentResolver().insert(CONTENT_URI, contentValues);
} }
public static Uri createFile(Context context, String targetName) { public static Uri createFile(Context context, String targetName) {
ContentValues contentValues = new ContentValues(); ContentValues contentValues = new ContentValues();
contentValues.put(COLUMN_NAME, targetName); contentValues.put(COLUMN_NAME, targetName);
return context.getContentResolver().insert(BASE_URI, contentValues); return context.getContentResolver().insert(CONTENT_URI, contentValues);
} }
public static Uri createFile(Context context) { public static Uri createFile(Context context) {
ContentValues contentValues = new ContentValues(); ContentValues contentValues = new ContentValues();
return context.getContentResolver().insert(BASE_URI, contentValues); return context.getContentResolver().insert(CONTENT_URI, contentValues);
} }
public static int setMimeType(Context context, Uri uri, String mimetype) { public static int setMimeType(Context context, Uri uri, String mimetype) {
@@ -98,7 +98,7 @@ public class TemporaryStorageProvider extends ContentProvider {
} }
public static int cleanUp(Context context) { public static int cleanUp(Context context) {
return context.getContentResolver().delete(BASE_URI, COLUMN_TIME + "< ?", return context.getContentResolver().delete(CONTENT_URI, COLUMN_TIME + "< ?",
new String[]{Long.toString(System.currentTimeMillis() - Constants.TEMPFILE_TTL)}); new String[]{Long.toString(System.currentTimeMillis() - Constants.TEMPFILE_TTL)});
} }
@@ -163,12 +163,19 @@ public class TemporaryStorageProvider extends ContentProvider {
throw new SecurityException("Listing temporary files is not allowed, only querying single files."); throw new SecurityException("Listing temporary files is not allowed, only querying single files.");
} }
Log.d(Constants.TAG, "being asked for file " + uri);
File file; File file;
try { try {
file = getFile(uri); file = getFile(uri);
if (file.exists()) {
Log.e(Constants.TAG, "already exists");
}
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
Log.e(Constants.TAG, "file not found!");
return null; return null;
} }
Cursor fileName = db.getReadableDatabase().query(TABLE_FILES, new String[]{COLUMN_NAME}, COLUMN_ID + "=?", Cursor fileName = db.getReadableDatabase().query(TABLE_FILES, new String[]{COLUMN_NAME}, COLUMN_ID + "=?",
new String[]{uri.getLastPathSegment()}, null, null, null); new String[]{uri.getLastPathSegment()}, null, null, null);
if (fileName != null) { if (fileName != null) {
@@ -236,7 +243,7 @@ public class TemporaryStorageProvider extends ContentProvider {
Log.e(Constants.TAG, "File creation failed!"); Log.e(Constants.TAG, "File creation failed!");
return null; return null;
} }
return Uri.withAppendedPath(BASE_URI, uuid); return Uri.withAppendedPath(CONTENT_URI, uuid);
} }
@Override @Override
@@ -274,6 +281,7 @@ public class TemporaryStorageProvider extends ContentProvider {
@Override @Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
Log.d(Constants.TAG, "openFile");
return openFileHelper(uri, mode); return openFileHelper(uri, mode);
} }

View File

@@ -628,15 +628,14 @@ public class OpenPgpService extends RemoteService {
} }
} }
if (data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) >= 4) {
OpenPgpMetadata metadata = pgpResult.getDecryptionMetadata(); OpenPgpMetadata metadata = pgpResult.getDecryptionMetadata();
if (data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) >= 4) {
if (metadata != null) { if (metadata != null) {
result.putExtra(OpenPgpApi.RESULT_METADATA, metadata); result.putExtra(OpenPgpApi.RESULT_METADATA, metadata);
} }
} }
String charset = pgpResult.getCharset(); String charset = metadata != null ? metadata.getCharset() : null;
if (charset != null) { if (charset != null) {
result.putExtra(OpenPgpApi.RESULT_CHARSET, charset); result.putExtra(OpenPgpApi.RESULT_CHARSET, charset);
} }

View File

@@ -0,0 +1,81 @@
/*
* Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel;
public class InputDataParcel implements Parcelable {
private Uri mInputUri;
private PgpDecryptVerifyInputParcel mDecryptInput;
private boolean mMimeDecode = true; // TODO default to false
public InputDataParcel(Uri inputUri, PgpDecryptVerifyInputParcel decryptInput) {
mInputUri = inputUri;
mDecryptInput = decryptInput;
}
InputDataParcel(Parcel source) {
// we do all of those here, so the PgpSignEncryptInput class doesn't have to be parcelable
mInputUri = source.readParcelable(getClass().getClassLoader());
mDecryptInput = source.readParcelable(getClass().getClassLoader());
mMimeDecode = source.readInt() != 0;
}
public Uri getInputUri() {
return mInputUri;
}
public PgpDecryptVerifyInputParcel getDecryptInput() {
return mDecryptInput;
}
public boolean getMimeDecode() {
return mMimeDecode;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeParcelable(mInputUri, 0);
dest.writeParcelable(mDecryptInput, 0);
dest.writeInt(mMimeDecode ? 1 : 0);
}
public static final Creator<InputDataParcel> CREATOR = new Creator<InputDataParcel>() {
public InputDataParcel createFromParcel(final Parcel source) {
return new InputDataParcel(source);
}
public InputDataParcel[] newArray(final int size) {
return new InputDataParcel[size];
}
};
}

View File

@@ -36,6 +36,7 @@ import org.sufficientlysecure.keychain.operations.EditKeyOperation;
import org.sufficientlysecure.keychain.operations.ExportOperation; import org.sufficientlysecure.keychain.operations.ExportOperation;
import org.sufficientlysecure.keychain.operations.ImportOperation; import org.sufficientlysecure.keychain.operations.ImportOperation;
import org.sufficientlysecure.keychain.operations.KeybaseVerificationOperation; import org.sufficientlysecure.keychain.operations.KeybaseVerificationOperation;
import org.sufficientlysecure.keychain.operations.InputDataOperation;
import org.sufficientlysecure.keychain.operations.PromoteKeyOperation; import org.sufficientlysecure.keychain.operations.PromoteKeyOperation;
import org.sufficientlysecure.keychain.operations.RevokeOperation; import org.sufficientlysecure.keychain.operations.RevokeOperation;
import org.sufficientlysecure.keychain.operations.SignEncryptOperation; import org.sufficientlysecure.keychain.operations.SignEncryptOperation;
@@ -108,35 +109,29 @@ public class KeychainService extends Service implements Progressable {
// just for brevity // just for brevity
KeychainService outerThis = KeychainService.this; KeychainService outerThis = KeychainService.this;
if (inputParcel instanceof SignEncryptParcel) { if (inputParcel instanceof SignEncryptParcel) {
op = new SignEncryptOperation(outerThis, new ProviderHelper(outerThis), op = new SignEncryptOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled);
outerThis, mActionCanceled);
} else if (inputParcel instanceof PgpDecryptVerifyInputParcel) { } else if (inputParcel instanceof PgpDecryptVerifyInputParcel) {
op = new PgpDecryptVerifyOperation(outerThis, new ProviderHelper(outerThis), outerThis); op = new PgpDecryptVerifyOperation(outerThis, new ProviderHelper(outerThis), outerThis);
} else if (inputParcel instanceof SaveKeyringParcel) { } else if (inputParcel instanceof SaveKeyringParcel) {
op = new EditKeyOperation(outerThis, new ProviderHelper(outerThis), outerThis, op = new EditKeyOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled);
mActionCanceled);
} else if (inputParcel instanceof RevokeKeyringParcel) { } else if (inputParcel instanceof RevokeKeyringParcel) {
op = new RevokeOperation(outerThis, new ProviderHelper(outerThis), outerThis); op = new RevokeOperation(outerThis, new ProviderHelper(outerThis), outerThis);
} else if (inputParcel instanceof CertifyActionsParcel) { } else if (inputParcel instanceof CertifyActionsParcel) {
op = new CertifyOperation(outerThis, new ProviderHelper(outerThis), outerThis, op = new CertifyOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled);
mActionCanceled);
} else if (inputParcel instanceof DeleteKeyringParcel) { } else if (inputParcel instanceof DeleteKeyringParcel) {
op = new DeleteOperation(outerThis, new ProviderHelper(outerThis), outerThis); op = new DeleteOperation(outerThis, new ProviderHelper(outerThis), outerThis);
} else if (inputParcel instanceof PromoteKeyringParcel) { } else if (inputParcel instanceof PromoteKeyringParcel) {
op = new PromoteKeyOperation(outerThis, new ProviderHelper(outerThis), op = new PromoteKeyOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled);
outerThis, mActionCanceled);
} else if (inputParcel instanceof ImportKeyringParcel) { } else if (inputParcel instanceof ImportKeyringParcel) {
op = new ImportOperation(outerThis, new ProviderHelper(outerThis), outerThis, op = new ImportOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled);
mActionCanceled);
} else if (inputParcel instanceof ExportKeyringParcel) { } else if (inputParcel instanceof ExportKeyringParcel) {
op = new ExportOperation(outerThis, new ProviderHelper(outerThis), outerThis, op = new ExportOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled);
mActionCanceled);
} else if (inputParcel instanceof ConsolidateInputParcel) { } else if (inputParcel instanceof ConsolidateInputParcel) {
op = new ConsolidateOperation(outerThis, new ProviderHelper(outerThis), op = new ConsolidateOperation(outerThis, new ProviderHelper(outerThis), outerThis);
outerThis);
} else if (inputParcel instanceof KeybaseVerificationParcel) { } else if (inputParcel instanceof KeybaseVerificationParcel) {
op = new KeybaseVerificationOperation(outerThis, new ProviderHelper(outerThis), op = new KeybaseVerificationOperation(outerThis, new ProviderHelper(outerThis), outerThis);
outerThis); } else if (inputParcel instanceof InputDataParcel) {
op = new InputDataOperation(outerThis, new ProviderHelper(outerThis), outerThis);
} else { } else {
throw new AssertionError("Unrecognized input parcel in KeychainService!"); throw new AssertionError("Unrecognized input parcel in KeychainService!");
} }

View File

@@ -82,6 +82,9 @@ public class DecryptActivity extends BaseActivity {
return; return;
} }
// depending on the data source, we may or may not be able to delete the original file
boolean canDelete = false;
try { try {
switch (action) { switch (action) {
@@ -152,10 +155,21 @@ public class DecryptActivity extends BaseActivity {
} }
// for everything else, just work on the intent data // for everything else, just work on the intent data
case OpenKeychainIntents.DECRYPT_DATA:
case Intent.ACTION_VIEW: case Intent.ACTION_VIEW:
canDelete = true;
case OpenKeychainIntents.DECRYPT_DATA:
default: default:
Uri uri = intent.getData();
if (uri != null) {
if ("com.android.email.attachmentprovider".equals(uri.getHost())) {
Toast.makeText(this, R.string.error_reading_aosp, Toast.LENGTH_LONG).show();
finish();
return;
}
uris.add(intent.getData()); uris.add(intent.getData());
}
} }
@@ -173,13 +187,17 @@ public class DecryptActivity extends BaseActivity {
return; return;
} }
displayListFragment(uris); displayListFragment(uris, canDelete);
} }
@Nullable public Uri readToTempFile(String text) throws IOException { @Nullable
public Uri readToTempFile(String text) throws IOException {
Uri tempFile = TemporaryStorageProvider.createFile(this); Uri tempFile = TemporaryStorageProvider.createFile(this);
OutputStream outStream = getContentResolver().openOutputStream(tempFile); OutputStream outStream = getContentResolver().openOutputStream(tempFile);
if (outStream == null) {
return null;
}
// clean up ascii armored message, fixing newlines and stuff // clean up ascii armored message, fixing newlines and stuff
String cleanedText = PgpHelper.getPgpContent(text); String cleanedText = PgpHelper.getPgpContent(text);
@@ -188,14 +206,14 @@ public class DecryptActivity extends BaseActivity {
} }
// if cleanup didn't work, just try the raw data // if cleanup didn't work, just try the raw data
outStream.write(text.getBytes()); outStream.write(cleanedText.getBytes());
outStream.close(); outStream.close();
return tempFile; return tempFile;
} }
public void displayListFragment(ArrayList<Uri> inputUris) { public void displayListFragment(ArrayList<Uri> inputUris, boolean canDelete) {
DecryptListFragment frag = DecryptListFragment.newInstance(inputUris); DecryptListFragment frag = DecryptListFragment.newInstance(inputUris, canDelete);
FragmentManager fragMan = getSupportFragmentManager(); FragmentManager fragMan = getSupportFragmentManager();

View File

@@ -28,6 +28,7 @@ import android.app.Activity;
import android.content.ClipDescription; import android.content.ClipDescription;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.LabeledIntent;
import android.content.pm.ResolveInfo; import android.content.pm.ResolveInfo;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Point; import android.graphics.Point;
@@ -36,6 +37,7 @@ import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable;
import android.support.v7.widget.DefaultItemAnimator; import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
@@ -44,26 +46,33 @@ import android.view.LayoutInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.webkit.MimeTypeMap;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu; import android.widget.PopupMenu;
import android.widget.PopupMenu.OnDismissListener; import android.widget.PopupMenu.OnDismissListener;
import android.widget.PopupMenu.OnMenuItemClickListener; import android.widget.PopupMenu.OnMenuItemClickListener;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import android.widget.ViewAnimator; import android.widget.ViewAnimator;
import com.cocosw.bottomsheet.BottomSheet;
import org.openintents.openpgp.OpenPgpMetadata; import org.openintents.openpgp.OpenPgpMetadata;
import org.openintents.openpgp.OpenPgpSignatureResult; import org.openintents.openpgp.OpenPgpSignatureResult;
import org.sufficientlysecure.keychain.BuildConfig;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; import org.sufficientlysecure.keychain.operations.results.InputDataResult;
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; import org.sufficientlysecure.keychain.service.InputDataParcel;
// this import NEEDS to be above the ViewModel one, or it won't compile! (as of 06/06/15)
import org.sufficientlysecure.keychain.ui.base.QueueingCryptoOperationFragment; import org.sufficientlysecure.keychain.ui.base.QueueingCryptoOperationFragment;
// this import NEEDS to be above the ViewModel AND SubViewHolder one, or it won't compile! (as of 16.09.15)
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.StatusHolder; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.StatusHolder;
import org.sufficientlysecure.keychain.ui.DecryptListFragment.ViewHolder.SubViewHolder;
import org.sufficientlysecure.keychain.ui.DecryptListFragment.DecryptFilesAdapter.ViewModel; import org.sufficientlysecure.keychain.ui.DecryptListFragment.DecryptFilesAdapter.ViewModel;
import org.sufficientlysecure.keychain.ui.adapter.SpacesItemDecoration; import org.sufficientlysecure.keychain.ui.adapter.SpacesItemDecoration;
import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
@@ -76,34 +85,38 @@ import org.sufficientlysecure.keychain.util.ParcelableHashMap;
public class DecryptListFragment public class DecryptListFragment
extends QueueingCryptoOperationFragment<PgpDecryptVerifyInputParcel,DecryptVerifyResult> extends QueueingCryptoOperationFragment<InputDataParcel,InputDataResult>
implements OnMenuItemClickListener { implements OnMenuItemClickListener {
public static final String ARG_INPUT_URIS = "input_uris"; public static final String ARG_INPUT_URIS = "input_uris";
public static final String ARG_OUTPUT_URIS = "output_uris"; public static final String ARG_OUTPUT_URIS = "output_uris";
public static final String ARG_CANCELLED_URIS = "cancelled_uris"; public static final String ARG_CANCELLED_URIS = "cancelled_uris";
public static final String ARG_RESULTS = "results"; public static final String ARG_RESULTS = "results";
public static final String ARG_CAN_DELETE = "can_delete";
private static final int REQUEST_CODE_OUTPUT = 0x00007007; private static final int REQUEST_CODE_OUTPUT = 0x00007007;
public static final String ARG_CURRENT_URI = "current_uri"; public static final String ARG_CURRENT_URI = "current_uri";
private ArrayList<Uri> mInputUris; private ArrayList<Uri> mInputUris;
private HashMap<Uri, Uri> mOutputUris; private HashMap<Uri, InputDataResult> mInputDataResults;
private ArrayList<Uri> mPendingInputUris; private ArrayList<Uri> mPendingInputUris;
private ArrayList<Uri> mCancelledInputUris; private ArrayList<Uri> mCancelledInputUris;
private Uri mCurrentInputUri; private Uri mCurrentInputUri;
private boolean mCanDelete;
private DecryptFilesAdapter mAdapter; private DecryptFilesAdapter mAdapter;
private Uri mCurrentSaveFileUri;
/** /**
* Creates new instance of this fragment * Creates new instance of this fragment
*/ */
public static DecryptListFragment newInstance(ArrayList<Uri> uris) { public static DecryptListFragment newInstance(ArrayList<Uri> uris, boolean canDelete) {
DecryptListFragment frag = new DecryptListFragment(); DecryptListFragment frag = new DecryptListFragment();
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putParcelableArrayList(ARG_INPUT_URIS, uris); args.putParcelableArrayList(ARG_INPUT_URIS, uris);
args.putBoolean(ARG_CAN_DELETE, canDelete);
frag.setArguments(args); frag.setArguments(args);
return frag; return frag;
@@ -129,7 +142,7 @@ public class DecryptListFragment
vFilesList.setLayoutManager(new LinearLayoutManager(getActivity())); vFilesList.setLayoutManager(new LinearLayoutManager(getActivity()));
vFilesList.setItemAnimator(new DefaultItemAnimator()); vFilesList.setItemAnimator(new DefaultItemAnimator());
mAdapter = new DecryptFilesAdapter(getActivity(), this); mAdapter = new DecryptFilesAdapter();
vFilesList.setAdapter(mAdapter); vFilesList.setAdapter(mAdapter);
return view; return view;
@@ -141,21 +154,22 @@ public class DecryptListFragment
outState.putParcelableArrayList(ARG_INPUT_URIS, mInputUris); outState.putParcelableArrayList(ARG_INPUT_URIS, mInputUris);
HashMap<Uri,DecryptVerifyResult> results = new HashMap<>(mInputUris.size()); HashMap<Uri,InputDataResult> results = new HashMap<>(mInputUris.size());
for (Uri uri : mInputUris) { for (Uri uri : mInputUris) {
if (mPendingInputUris.contains(uri)) { if (mPendingInputUris.contains(uri)) {
continue; continue;
} }
DecryptVerifyResult result = mAdapter.getItemResult(uri); InputDataResult result = mAdapter.getItemResult(uri);
if (result != null) { if (result != null) {
results.put(uri, result); results.put(uri, result);
} }
} }
outState.putParcelable(ARG_RESULTS, new ParcelableHashMap<>(results)); outState.putParcelable(ARG_RESULTS, new ParcelableHashMap<>(results));
outState.putParcelable(ARG_OUTPUT_URIS, new ParcelableHashMap<>(mOutputUris)); outState.putParcelable(ARG_OUTPUT_URIS, new ParcelableHashMap<>(mInputDataResults));
outState.putParcelableArrayList(ARG_CANCELLED_URIS, mCancelledInputUris); outState.putParcelableArrayList(ARG_CANCELLED_URIS, mCancelledInputUris);
outState.putParcelable(ARG_CURRENT_URI, mCurrentInputUri); outState.putParcelable(ARG_CURRENT_URI, mCurrentInputUri);
outState.putBoolean(ARG_CAN_DELETE, mCanDelete);
} }
@@ -167,23 +181,22 @@ public class DecryptListFragment
ArrayList<Uri> inputUris = getArguments().getParcelableArrayList(ARG_INPUT_URIS); ArrayList<Uri> inputUris = getArguments().getParcelableArrayList(ARG_INPUT_URIS);
ArrayList<Uri> cancelledUris = args.getParcelableArrayList(ARG_CANCELLED_URIS); ArrayList<Uri> cancelledUris = args.getParcelableArrayList(ARG_CANCELLED_URIS);
ParcelableHashMap<Uri,Uri> outputUris = args.getParcelable(ARG_OUTPUT_URIS); ParcelableHashMap<Uri,InputDataResult> results = args.getParcelable(ARG_RESULTS);
ParcelableHashMap<Uri,DecryptVerifyResult> results = args.getParcelable(ARG_RESULTS);
Uri currentInputUri = args.getParcelable(ARG_CURRENT_URI); Uri currentInputUri = args.getParcelable(ARG_CURRENT_URI);
mCanDelete = args.getBoolean(ARG_CAN_DELETE, false);
displayInputUris(inputUris, currentInputUri, cancelledUris, displayInputUris(inputUris, currentInputUri, cancelledUris,
outputUris != null ? outputUris.getMap() : null,
results != null ? results.getMap() : null results != null ? results.getMap() : null
); );
} }
private void displayInputUris(ArrayList<Uri> inputUris, Uri currentInputUri, private void displayInputUris(ArrayList<Uri> inputUris, Uri currentInputUri,
ArrayList<Uri> cancelledUris, HashMap<Uri,Uri> outputUris, ArrayList<Uri> cancelledUris, HashMap<Uri,InputDataResult> results) {
HashMap<Uri,DecryptVerifyResult> results) {
mInputUris = inputUris; mInputUris = inputUris;
mCurrentInputUri = currentInputUri; mCurrentInputUri = currentInputUri;
mOutputUris = outputUris != null ? outputUris : new HashMap<Uri,Uri>(inputUris.size()); mInputDataResults = results != null ? results : new HashMap<Uri,InputDataResult>(inputUris.size());
mCancelledInputUris = cancelledUris != null ? cancelledUris : new ArrayList<Uri>(); mCancelledInputUris = cancelledUris != null ? cancelledUris : new ArrayList<Uri>();
mPendingInputUris = new ArrayList<>(); mPendingInputUris = new ArrayList<>();
@@ -206,9 +219,8 @@ public class DecryptListFragment
} }
if (results != null && results.containsKey(uri)) { if (results != null && results.containsKey(uri)) {
processResult(uri, results.get(uri)); processResult(uri);
} else { } else {
mOutputUris.put(uri, TemporaryStorageProvider.createFile(getActivity()));
mPendingInputUris.add(uri); mPendingInputUris.add(uri);
} }
} }
@@ -224,9 +236,8 @@ public class DecryptListFragment
case REQUEST_CODE_OUTPUT: { case REQUEST_CODE_OUTPUT: {
// This happens after output file was selected, so start our operation // This happens after output file was selected, so start our operation
if (resultCode == Activity.RESULT_OK && data != null) { if (resultCode == Activity.RESULT_OK && data != null) {
Uri decryptedFileUri = mOutputUris.get(mCurrentInputUri);
Uri saveUri = data.getData(); Uri saveUri = data.getData();
saveFile(decryptedFileUri, saveUri); saveFile(saveUri);
mCurrentInputUri = null; mCurrentInputUri = null;
} }
return; return;
@@ -238,7 +249,37 @@ public class DecryptListFragment
} }
} }
private void saveFile(Uri decryptedFileUri, Uri saveUri) { private void saveFileDialog(InputDataResult result, int index) {
Activity activity = getActivity();
if (activity == null) {
return;
}
OpenPgpMetadata metadata = result.mMetadata.get(index);
Uri saveUri = Uri.fromFile(activity.getExternalFilesDir(metadata.getMimeType()));
mCurrentSaveFileUri = result.getOutputUris().get(index);
String filename = metadata.getFilename();
if (filename == null) {
String ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(metadata.getMimeType());
filename = "decrypted" + (ext != null ? "."+ext : "");
}
FileHelper.saveDocument(this, filename, saveUri, metadata.getMimeType(),
R.string.title_decrypt_to_file, R.string.specify_file_to_decrypt_to, REQUEST_CODE_OUTPUT);
}
private void saveFile(Uri saveUri) {
if (mCurrentSaveFileUri == null) {
return;
}
Uri decryptedFileUri = mCurrentSaveFileUri;
mCurrentInputUri = null;
hideKeyboard();
Activity activity = getActivity(); Activity activity = getActivity();
if (activity == null) { if (activity == null) {
return; return;
@@ -260,21 +301,27 @@ public class DecryptListFragment
} }
@Override @Override
public void onQueuedOperationError(DecryptVerifyResult result) { public void onQueuedOperationError(InputDataResult result) {
final Uri uri = mCurrentInputUri; final Uri uri = mCurrentInputUri;
mCurrentInputUri = null; mCurrentInputUri = null;
mAdapter.addResult(uri, result, null, null, null); Activity activity = getActivity();
if (activity != null && "com.fsck.k9.attachmentprovider".equals(uri.getHost())) {
Toast.makeText(getActivity(), R.string.error_reading_k9, Toast.LENGTH_LONG).show();
}
mAdapter.addResult(uri, result);
cryptoOperation(); cryptoOperation();
} }
@Override @Override
public void onQueuedOperationSuccess(DecryptVerifyResult result) { public void onQueuedOperationSuccess(InputDataResult result) {
Uri uri = mCurrentInputUri; Uri uri = mCurrentInputUri;
mCurrentInputUri = null; mCurrentInputUri = null;
processResult(uri, result); mInputDataResults.put(uri, result);
processResult(uri);
cryptoOperation(); cryptoOperation();
} }
@@ -298,31 +345,41 @@ public class DecryptListFragment
} }
private void processResult(final Uri uri, final DecryptVerifyResult result) { HashMap<Uri,Drawable> mIconCache = new HashMap<>();
new AsyncTask<Void, Void, Drawable>() { private void processResult(final Uri uri) {
new AsyncTask<Void, Void, Void>() {
@Override @Override
protected Drawable doInBackground(Void... params) { protected Void doInBackground(Void... params) {
InputDataResult result = mInputDataResults.get(uri);
Context context = getActivity(); Context context = getActivity();
if (result.getDecryptionMetadata() == null || context == null) { if (context == null) {
return null; return null;
} }
String type = result.getDecryptionMetadata().getMimeType(); for (int i = 0; i < result.getOutputUris().size(); i++) {
Uri outputUri = mOutputUris.get(uri);
if (type == null || outputUri == null) { Uri outputUri = result.getOutputUris().get(i);
return null; if (mIconCache.containsKey(outputUri)) {
continue;
} }
TemporaryStorageProvider.setMimeType(context, outputUri, type); OpenPgpMetadata metadata = result.mMetadata.get(i);
String type = metadata.getMimeType();
if (ClipDescription.compareMimeTypes(type, "image/*")) { Drawable icon = null;
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, "image/*")) {
int px = FormattingUtils.dpToPx(context, 48); int px = FormattingUtils.dpToPx(context, 48);
Bitmap bitmap = FileHelper.getThumbnail(context, outputUri, new Point(px, px)); Bitmap bitmap = FileHelper.getThumbnail(context, outputUri, new Point(px, px));
return new BitmapDrawable(context.getResources(), bitmap); icon = new BitmapDrawable(context.getResources(), bitmap);
} } else {
final Intent intent = new Intent(Intent.ACTION_VIEW); final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(outputUri, type); intent.setDataAndType(outputUri, type);
@@ -330,7 +387,15 @@ public class DecryptListFragment
context.getPackageManager().queryIntentActivities(intent, 0); context.getPackageManager().queryIntentActivities(intent, 0);
// noinspection LoopStatementThatDoesntLoop // noinspection LoopStatementThatDoesntLoop
for (ResolveInfo match : matches) { for (ResolveInfo match : matches) {
return match.loadIcon(getActivity().getPackageManager()); icon = match.loadIcon(getActivity().getPackageManager());
break;
}
}
if (icon != null) {
mIconCache.put(outputUri, icon);
}
} }
return null; return null;
@@ -338,49 +403,14 @@ public class DecryptListFragment
} }
@Override @Override
protected void onPostExecute(Drawable icon) { protected void onPostExecute(Void v) {
processResult(uri, result, icon); InputDataResult result = mInputDataResults.get(uri);
mAdapter.addResult(uri, result);
} }
}.execute(); }.execute();
} }
private void processResult(final Uri uri, DecryptVerifyResult result, Drawable icon) {
OnClickListener onFileClick = null, onKeyClick = null;
OpenPgpSignatureResult sigResult = result.getSignatureResult();
if (sigResult != null) {
final long keyId = sigResult.getKeyId();
if (sigResult.getResult() != OpenPgpSignatureResult.RESULT_KEY_MISSING) {
onKeyClick = new OnClickListener() {
@Override
public void onClick(View view) {
Activity activity = getActivity();
if (activity == null) {
return;
}
Intent intent = new Intent(activity, ViewKeyActivity.class);
intent.setData(KeyRings.buildUnifiedKeyRingUri(keyId));
activity.startActivity(intent);
}
};
}
}
if (result.success() && result.getDecryptionMetadata() != null) {
onFileClick = new OnClickListener() {
@Override
public void onClick(View view) {
displayWithViewIntent(uri, false);
}
};
}
mAdapter.addResult(uri, result, icon, onFileClick, onKeyClick);
}
public void retryUri(Uri uri) { public void retryUri(Uri uri) {
// never interrupt running operations! // never interrupt running operations!
@@ -397,19 +427,41 @@ public class DecryptListFragment
} }
public void displayWithViewIntent(final Uri uri, boolean share) { public void displayBottomSheet(final InputDataResult result, final int index) {
Activity activity = getActivity(); Activity activity = getActivity();
if (activity == null || mCurrentInputUri != null) { if (activity == null) {
return; return;
} }
final Uri outputUri = mOutputUris.get(uri); new BottomSheet.Builder(activity).sheet(R.menu.decrypt_bottom_sheet).listener(new MenuItem.OnMenuItemClickListener() {
final DecryptVerifyResult result = mAdapter.getItemResult(uri); @Override
if (outputUri == null || result == null) { public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.decrypt_open:
displayWithViewIntent(result, index, false, true);
break;
case R.id.decrypt_share:
displayWithViewIntent(result, index, true, true);
break;
case R.id.decrypt_save:
saveFileDialog(result, index);
break;
}
return false;
}
}).grid().show();
}
public void displayWithViewIntent(InputDataResult result, int index, boolean share, boolean forceChooser) {
Activity activity = getActivity();
if (activity == null) {
return; return;
} }
final OpenPgpMetadata metadata = result.getDecryptionMetadata(); Uri outputUri = result.getOutputUris().get(index);
OpenPgpMetadata metadata = result.mMetadata.get(index);
// text/plain is a special case where we extract the uri content into // text/plain is a special case where we extract the uri content into
// the EXTRA_TEXT extra ourselves, and display a chooser which includes // the EXTRA_TEXT extra ourselves, and display a chooser which includes
@@ -418,12 +470,14 @@ public class DecryptListFragment
if (share) { if (share) {
try { try {
String plaintext = FileHelper.readTextFromUri(activity, outputUri, result.getCharset()); String plaintext = FileHelper.readTextFromUri(activity, outputUri, null);
Intent intent = new Intent(Intent.ACTION_SEND); Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType(metadata.getMimeType()); intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, plaintext); intent.putExtra(Intent.EXTRA_TEXT, plaintext);
startActivity(intent);
Intent chooserIntent = Intent.createChooser(intent, getString(R.string.intent_share));
startActivity(chooserIntent);
} catch (IOException e) { } catch (IOException e) {
Notify.create(activity, R.string.error_preparing_data, Style.ERROR).show(); Notify.create(activity, R.string.error_preparing_data, Style.ERROR).show();
@@ -432,11 +486,34 @@ public class DecryptListFragment
return; return;
} }
Intent intent = new Intent(activity, DisplayTextActivity.class); Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW); intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(outputUri, metadata.getMimeType()); intent.setDataAndType(outputUri, "text/plain");
intent.putExtra(DisplayTextActivity.EXTRA_METADATA, result);
activity.startActivity(intent); if (forceChooser) {
LabeledIntent internalIntent = new LabeledIntent(
new Intent(intent)
.setClass(activity, DisplayTextActivity.class)
.putExtra(DisplayTextActivity.EXTRA_RESULT, result.mDecryptVerifyResult)
.putExtra(DisplayTextActivity.EXTRA_METADATA, metadata),
BuildConfig.APPLICATION_ID, R.string.view_internal, R.mipmap.ic_launcher);
Intent chooserIntent = Intent.createChooser(intent, getString(R.string.intent_show));
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS,
new Parcelable[] { internalIntent });
startActivity(chooserIntent);
} else {
intent.setClass(activity, DisplayTextActivity.class);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.putExtra(DisplayTextActivity.EXTRA_RESULT, result.mDecryptVerifyResult);
intent.putExtra(DisplayTextActivity.EXTRA_METADATA, metadata);
startActivity(intent);
}
} else { } else {
@@ -454,13 +531,13 @@ public class DecryptListFragment
Intent chooserIntent = Intent.createChooser(intent, getString(R.string.intent_show)); Intent chooserIntent = Intent.createChooser(intent, getString(R.string.intent_show));
chooserIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); chooserIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
activity.startActivity(chooserIntent); startActivity(chooserIntent);
} }
} }
@Override @Override
public PgpDecryptVerifyInputParcel createOperationInput() { public InputDataParcel createOperationInput() {
if (mCurrentInputUri == null) { if (mCurrentInputUri == null) {
if (mPendingInputUris.isEmpty()) { if (mPendingInputUris.isEmpty()) {
@@ -471,11 +548,11 @@ public class DecryptListFragment
mCurrentInputUri = mPendingInputUris.remove(0); mCurrentInputUri = mPendingInputUris.remove(0);
} }
Uri currentOutputUri = mOutputUris.get(mCurrentInputUri); Log.d(Constants.TAG, "mInputUri=" + mCurrentInputUri);
Log.d(Constants.TAG, "mInputUri=" + mCurrentInputUri + ", mOutputUri=" + currentOutputUri);
return new PgpDecryptVerifyInputParcel(mCurrentInputUri, currentOutputUri) PgpDecryptVerifyInputParcel decryptInput = new PgpDecryptVerifyInputParcel()
.setAllowSymmetricDecryption(true); .setAllowSymmetricDecryption(true);
return new InputDataParcel(mCurrentInputUri, decryptInput);
} }
@@ -496,25 +573,12 @@ public class DecryptListFragment
} }
ViewModel model = mAdapter.mMenuClickedModel; ViewModel model = mAdapter.mMenuClickedModel;
DecryptVerifyResult result = model.mResult;
switch (menuItem.getItemId()) { switch (menuItem.getItemId()) {
case R.id.view_log: case R.id.view_log:
Intent intent = new Intent(activity, LogDisplayActivity.class); Intent intent = new Intent(activity, LogDisplayActivity.class);
intent.putExtra(LogDisplayFragment.EXTRA_RESULT, result); intent.putExtra(LogDisplayFragment.EXTRA_RESULT, model.mResult);
activity.startActivity(intent); activity.startActivity(intent);
return true; return true;
case R.id.decrypt_share:
displayWithViewIntent(model.mInputUri, true);
return true;
case R.id.decrypt_save:
OpenPgpMetadata metadata = result.getDecryptionMetadata();
if (metadata == null) {
return true;
}
mCurrentInputUri = model.mInputUri;
FileHelper.saveDocument(this, metadata.getFilename(), model.mInputUri, metadata.getMimeType(),
R.string.title_decrypt_to_file, R.string.specify_file_to_decrypt_to, REQUEST_CODE_OUTPUT);
return true;
case R.id.decrypt_delete: case R.id.decrypt_delete:
deleteFile(activity, model.mInputUri); deleteFile(activity, model.mInputUri);
return true; return true;
@@ -524,6 +588,9 @@ public class DecryptListFragment
private void deleteFile(Activity activity, Uri uri) { private void deleteFile(Activity activity, Uri uri) {
// we can only ever delete a file once, if we got this far either it's gone or it will never work
mCanDelete = false;
if ("file".equals(uri.getScheme())) { if ("file".equals(uri.getScheme())) {
File file = new File(uri.getPath()); File file = new File(uri.getPath());
if (file.delete()) { if (file.delete()) {
@@ -553,46 +620,29 @@ public class DecryptListFragment
} }
public static class DecryptFilesAdapter extends RecyclerView.Adapter<ViewHolder> { public class DecryptFilesAdapter extends RecyclerView.Adapter<ViewHolder> {
private Context mContext;
private ArrayList<ViewModel> mDataset; private ArrayList<ViewModel> mDataset;
private OnMenuItemClickListener mMenuItemClickListener;
private ViewModel mMenuClickedModel; private ViewModel mMenuClickedModel;
public class ViewModel { public class ViewModel {
Context mContext;
Uri mInputUri; Uri mInputUri;
DecryptVerifyResult mResult; InputDataResult mResult;
Drawable mIcon;
OnClickListener mOnFileClickListener;
OnClickListener mOnKeyClickListener;
int mProgress, mMax; int mProgress, mMax;
String mProgressMsg; String mProgressMsg;
OnClickListener mCancelled; OnClickListener mCancelled;
ViewModel(Context context, Uri uri) { ViewModel(Uri uri) {
mContext = context;
mInputUri = uri; mInputUri = uri;
mProgress = 0; mProgress = 0;
mMax = 100; mMax = 100;
mCancelled = null; mCancelled = null;
} }
void addResult(DecryptVerifyResult result) { void addResult(InputDataResult result) {
mResult = result; mResult = result;
} }
void addIcon(Drawable icon) {
mIcon = icon;
}
void setOnClickListeners(OnClickListener onFileClick, OnClickListener onKeyClick) {
mOnFileClickListener = onFileClick;
mOnKeyClickListener = onKeyClick;
}
boolean hasResult() { boolean hasResult() {
return mResult != null; return mResult != null;
} }
@@ -636,9 +686,7 @@ public class DecryptListFragment
} }
// Provide a suitable constructor (depends on the kind of dataset) // Provide a suitable constructor (depends on the kind of dataset)
public DecryptFilesAdapter(Context context, OnMenuItemClickListener menuItemClickListener) { public DecryptFilesAdapter() {
mContext = context;
mMenuItemClickListener = menuItemClickListener;
mDataset = new ArrayList<>(); mDataset = new ArrayList<>();
} }
@@ -701,51 +749,103 @@ public class DecryptListFragment
holder.vAnimator.setDisplayedChild(1); holder.vAnimator.setDisplayedChild(1);
} }
KeyFormattingUtils.setStatus(mContext, holder, model.mResult); KeyFormattingUtils.setStatus(getResources(), holder, model.mResult.mDecryptVerifyResult);
final OpenPgpMetadata metadata = model.mResult.getDecryptionMetadata(); int numFiles = model.mResult.getOutputUris().size();
holder.resizeFileList(numFiles, LayoutInflater.from(getActivity()));
for (int i = 0; i < numFiles; i++) {
Uri outputUri = model.mResult.getOutputUris().get(i);
OpenPgpMetadata metadata = model.mResult.mMetadata.get(i);
SubViewHolder fileHolder = holder.mFileHolderList.get(i);
String filename; String filename;
if (metadata == null) { if (metadata == null) {
filename = mContext.getString(R.string.filename_unknown); filename = getString(R.string.filename_unknown);
} else if (TextUtils.isEmpty(metadata.getFilename())) { } else if (TextUtils.isEmpty(metadata.getFilename())) {
filename = mContext.getString("text/plain".equals(metadata.getMimeType()) filename = getString("text/plain".equals(metadata.getMimeType())
? R.string.filename_unknown_text : R.string.filename_unknown); ? R.string.filename_unknown_text : R.string.filename_unknown);
} else { } else {
filename = metadata.getFilename(); filename = metadata.getFilename();
} }
holder.vFilename.setText(filename); fileHolder.vFilename.setText(filename);
long size = metadata == null ? 0 : metadata.getOriginalSize(); long size = metadata == null ? 0 : metadata.getOriginalSize();
if (size == -1 || size == 0) { if (size == -1 || size == 0) {
holder.vFilesize.setText(""); fileHolder.vFilesize.setText("");
} else { } else {
holder.vFilesize.setText(FileHelper.readableFileSize(size)); fileHolder.vFilesize.setText(FileHelper.readableFileSize(size));
} }
if (model.mIcon != null) { if (mIconCache.containsKey(outputUri)) {
holder.vThumbnail.setImageDrawable(model.mIcon); fileHolder.vThumbnail.setImageDrawable(mIconCache.get(outputUri));
} else { } else {
holder.vThumbnail.setImageResource(R.drawable.ic_doc_generic_am); fileHolder.vThumbnail.setImageResource(R.drawable.ic_doc_generic_am);
} }
holder.vFile.setOnClickListener(model.mOnFileClickListener); // save index closure-style :)
holder.vSignatureLayout.setOnClickListener(model.mOnKeyClickListener); final int idx = i;
fileHolder.vFile.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
if (model.mResult.success()) {
displayBottomSheet(model.mResult, idx);
return true;
}
return false;
}
});
fileHolder.vFile.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
if (model.mResult.success()) {
displayWithViewIntent(model.mResult, idx, false, false);
}
}
});
}
OpenPgpSignatureResult sigResult = model.mResult.mDecryptVerifyResult.getSignatureResult();
if (sigResult != null) {
final long keyId = sigResult.getKeyId();
if (sigResult.getResult() != OpenPgpSignatureResult.RESULT_KEY_MISSING) {
holder.vSignatureLayout.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
Activity activity = getActivity();
if (activity == null) {
return;
}
Intent intent = new Intent(activity, ViewKeyActivity.class);
intent.setData(KeyRings.buildUnifiedKeyRingUri(keyId));
activity.startActivity(intent);
}
});
}
}
holder.vContextMenu.setTag(model); holder.vContextMenu.setTag(model);
holder.vContextMenu.setOnClickListener(new OnClickListener() { holder.vContextMenu.setOnClickListener(new OnClickListener() {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
Activity activity = getActivity();
if (activity == null) {
return;
}
mMenuClickedModel = model; mMenuClickedModel = model;
PopupMenu menu = new PopupMenu(mContext, view); PopupMenu menu = new PopupMenu(activity, view);
menu.inflate(R.menu.decrypt_item_context_menu); menu.inflate(R.menu.decrypt_item_context_menu);
menu.setOnMenuItemClickListener(mMenuItemClickListener); menu.setOnMenuItemClickListener(DecryptListFragment.this);
menu.setOnDismissListener(new OnDismissListener() { menu.setOnDismissListener(new OnDismissListener() {
@Override @Override
public void onDismiss(PopupMenu popupMenu) { public void onDismiss(PopupMenu popupMenu) {
mMenuClickedModel = null; mMenuClickedModel = null;
} }
}); });
menu.getMenu().findItem(R.id.decrypt_delete).setEnabled(mCanDelete);
menu.show(); menu.show();
} }
}); });
@@ -761,9 +861,13 @@ public class DecryptListFragment
holder.vErrorViewLog.setOnClickListener(new OnClickListener() { holder.vErrorViewLog.setOnClickListener(new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
Intent intent = new Intent(mContext, LogDisplayActivity.class); Activity activity = getActivity();
if (activity == null) {
return;
}
Intent intent = new Intent(activity, LogDisplayActivity.class);
intent.putExtra(LogDisplayFragment.EXTRA_RESULT, model.mResult); intent.putExtra(LogDisplayFragment.EXTRA_RESULT, model.mResult);
mContext.startActivity(intent); activity.startActivity(intent);
} }
}); });
@@ -775,8 +879,8 @@ public class DecryptListFragment
return mDataset.size(); return mDataset.size();
} }
public DecryptVerifyResult getItemResult(Uri uri) { public InputDataResult getItemResult(Uri uri) {
ViewModel model = new ViewModel(mContext, uri); ViewModel model = new ViewModel(uri);
int pos = mDataset.indexOf(model); int pos = mDataset.indexOf(model);
if (pos == -1) { if (pos == -1) {
return null; return null;
@@ -787,37 +891,32 @@ public class DecryptListFragment
} }
public void add(Uri uri) { public void add(Uri uri) {
ViewModel newModel = new ViewModel(mContext, uri); ViewModel newModel = new ViewModel(uri);
mDataset.add(newModel); mDataset.add(newModel);
notifyItemInserted(mDataset.size()); notifyItemInserted(mDataset.size());
} }
public void setProgress(Uri uri, int progress, int max, String msg) { public void setProgress(Uri uri, int progress, int max, String msg) {
ViewModel newModel = new ViewModel(mContext, uri); ViewModel newModel = new ViewModel(uri);
int pos = mDataset.indexOf(newModel); int pos = mDataset.indexOf(newModel);
mDataset.get(pos).setProgress(progress, max, msg); mDataset.get(pos).setProgress(progress, max, msg);
notifyItemChanged(pos); notifyItemChanged(pos);
} }
public void setCancelled(Uri uri, OnClickListener retryListener) { public void setCancelled(Uri uri, OnClickListener retryListener) {
ViewModel newModel = new ViewModel(mContext, uri); ViewModel newModel = new ViewModel(uri);
int pos = mDataset.indexOf(newModel); int pos = mDataset.indexOf(newModel);
mDataset.get(pos).setCancelled(retryListener); mDataset.get(pos).setCancelled(retryListener);
notifyItemChanged(pos); notifyItemChanged(pos);
} }
public void addResult(Uri uri, DecryptVerifyResult result, Drawable icon, public void addResult(Uri uri, InputDataResult result) {
OnClickListener onFileClick, OnClickListener onKeyClick) {
ViewModel model = new ViewModel(mContext, uri); ViewModel model = new ViewModel(uri);
int pos = mDataset.indexOf(model); int pos = mDataset.indexOf(model);
model = mDataset.get(pos); model = mDataset.get(pos);
model.addResult(result); model.addResult(result);
if (icon != null) {
model.addIcon(icon);
}
model.setOnClickListeners(onFileClick, onKeyClick);
notifyItemChanged(pos); notifyItemChanged(pos);
} }
@@ -834,11 +933,6 @@ public class DecryptListFragment
public ProgressBar vProgress; public ProgressBar vProgress;
public TextView vProgressMsg; public TextView vProgressMsg;
public View vFile;
public TextView vFilename;
public TextView vFilesize;
public ImageView vThumbnail;
public ImageView vEncStatusIcon; public ImageView vEncStatusIcon;
public TextView vEncStatusText; public TextView vEncStatusText;
@@ -855,6 +949,25 @@ public class DecryptListFragment
public ImageView vCancelledRetry; public ImageView vCancelledRetry;
public LinearLayout vFileList;
public static class SubViewHolder {
public View vFile;
public TextView vFilename;
public TextView vFilesize;
public ImageView vThumbnail;
public SubViewHolder(View itemView) {
vFile = itemView.findViewById(R.id.file);
vFilename = (TextView) itemView.findViewById(R.id.filename);
vFilesize = (TextView) itemView.findViewById(R.id.filesize);
vThumbnail = (ImageView) itemView.findViewById(R.id.thumbnail);
}
}
public ArrayList<SubViewHolder> mFileHolderList = new ArrayList<>();
private int mCurrentFileListSize = 0;
public ViewHolder(View itemView) { public ViewHolder(View itemView) {
super(itemView); super(itemView);
@@ -863,11 +976,6 @@ public class DecryptListFragment
vProgress = (ProgressBar) itemView.findViewById(R.id.progress); vProgress = (ProgressBar) itemView.findViewById(R.id.progress);
vProgressMsg = (TextView) itemView.findViewById(R.id.progress_msg); vProgressMsg = (TextView) itemView.findViewById(R.id.progress_msg);
vFile = itemView.findViewById(R.id.file);
vFilename = (TextView) itemView.findViewById(R.id.filename);
vFilesize = (TextView) itemView.findViewById(R.id.filesize);
vThumbnail = (ImageView) itemView.findViewById(R.id.thumbnail);
vEncStatusIcon = (ImageView) itemView.findViewById(R.id.result_encryption_icon); vEncStatusIcon = (ImageView) itemView.findViewById(R.id.result_encryption_icon);
vEncStatusText = (TextView) itemView.findViewById(R.id.result_encryption_text); vEncStatusText = (TextView) itemView.findViewById(R.id.result_encryption_text);
@@ -878,6 +986,12 @@ public class DecryptListFragment
vSignatureMail= (TextView) itemView.findViewById(R.id.result_signature_email); vSignatureMail= (TextView) itemView.findViewById(R.id.result_signature_email);
vSignatureAction = (TextView) itemView.findViewById(R.id.result_signature_action); vSignatureAction = (TextView) itemView.findViewById(R.id.result_signature_action);
vFileList = (LinearLayout) itemView.findViewById(R.id.file_list);
for (int i = 0; i < vFileList.getChildCount(); i++) {
mFileHolderList.add(new SubViewHolder(vFileList.getChildAt(i)));
mCurrentFileListSize += 1;
}
vContextMenu = itemView.findViewById(R.id.context_menu); vContextMenu = itemView.findViewById(R.id.context_menu);
vErrorMsg = (TextView) itemView.findViewById(R.id.result_error_msg); vErrorMsg = (TextView) itemView.findViewById(R.id.result_error_msg);
@@ -887,6 +1001,27 @@ public class DecryptListFragment
} }
public void resizeFileList(int size, LayoutInflater inflater) {
int childCount = vFileList.getChildCount();
// if we require more children, create them
while (childCount < size) {
View v = inflater.inflate(R.layout.decrypt_list_file_item, null);
vFileList.addView(v);
mFileHolderList.add(new SubViewHolder(v));
childCount += 1;
}
while (size < mCurrentFileListSize) {
mCurrentFileListSize -= 1;
vFileList.getChildAt(mCurrentFileListSize).setVisibility(View.GONE);
}
while (size > mCurrentFileListSize) {
vFileList.getChildAt(mCurrentFileListSize).setVisibility(View.VISIBLE);
mCurrentFileListSize += 1;
}
}
@Override @Override
public ImageView getEncryptionStatusIcon() { public ImageView getEncryptionStatusIcon() {
return vEncStatusIcon; return vEncStatusIcon;

View File

@@ -25,9 +25,9 @@ import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.view.View;
import android.widget.Toast; import android.widget.Toast;
import org.openintents.openpgp.OpenPgpMetadata;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult;
import org.sufficientlysecure.keychain.ui.base.BaseActivity; import org.sufficientlysecure.keychain.ui.base.BaseActivity;
@@ -35,6 +35,7 @@ import org.sufficientlysecure.keychain.util.FileHelper;
public class DisplayTextActivity extends BaseActivity { public class DisplayTextActivity extends BaseActivity {
public static final String EXTRA_RESULT = "result";
public static final String EXTRA_METADATA = "metadata"; public static final String EXTRA_METADATA = "metadata";
@Override @Override
@@ -60,11 +61,12 @@ public class DisplayTextActivity extends BaseActivity {
return; return;
} }
DecryptVerifyResult result = intent.getParcelableExtra(EXTRA_METADATA); DecryptVerifyResult result = intent.getParcelableExtra(EXTRA_RESULT);
OpenPgpMetadata metadata = intent.getParcelableExtra(EXTRA_METADATA);
String plaintext; String plaintext;
try { try {
plaintext = FileHelper.readTextFromUri(this, intent.getData(), result.getCharset()); plaintext = FileHelper.readTextFromUri(this, intent.getData(), metadata.getCharset());
} catch (IOException e) { } catch (IOException e) {
Toast.makeText(this, R.string.error_preparing_data, Toast.LENGTH_LONG).show(); Toast.makeText(this, R.string.error_preparing_data, Toast.LENGTH_LONG).show();
return; return;

View File

@@ -19,6 +19,7 @@
package org.sufficientlysecure.keychain.ui.base; package org.sufficientlysecure.keychain.ui.base;
import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Parcelable; import android.os.Parcelable;
@@ -116,14 +117,15 @@ public abstract class CryptoOperationFragment<T extends Parcelable, S extends Op
} }
public void hideKeyboard() { public void hideKeyboard() {
if (getActivity() == null) { Activity activity = getActivity();
if (activity == null) {
return; return;
} }
InputMethodManager inputManager = (InputMethodManager) getActivity() InputMethodManager inputManager = (InputMethodManager) activity
.getSystemService(Context.INPUT_METHOD_SERVICE); .getSystemService(Context.INPUT_METHOD_SERVICE);
// check if no view has focus // check if no view has focus
View v = getActivity().getCurrentFocus(); View v = activity.getCurrentFocus();
if (v == null) if (v == null)
return; return;

View File

@@ -19,6 +19,7 @@
package org.sufficientlysecure.keychain.ui.util; package org.sufficientlysecure.keychain.ui.util;
import android.content.Context; import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.text.Spannable; import android.text.Spannable;
@@ -446,7 +447,7 @@ public class KeyFormattingUtils {
} }
@SuppressWarnings("deprecation") // context.getDrawable is api lvl 21, need to use deprecated @SuppressWarnings("deprecation") // context.getDrawable is api lvl 21, need to use deprecated
public static void setStatus(Context context, StatusHolder holder, DecryptVerifyResult result) { public static void setStatus(Resources resources, StatusHolder holder, DecryptVerifyResult result) {
if (holder.hasEncrypt()) { if (holder.hasEncrypt()) {
OpenPgpDecryptionResult decryptionResult = result.getDecryptionResult(); OpenPgpDecryptionResult decryptionResult = result.getDecryptionResult();
@@ -477,9 +478,9 @@ public class KeyFormattingUtils {
} }
} }
int encColorRes = context.getResources().getColor(encColor); int encColorRes = resources.getColor(encColor);
holder.getEncryptionStatusIcon().setColorFilter(encColorRes, PorterDuff.Mode.SRC_IN); holder.getEncryptionStatusIcon().setColorFilter(encColorRes, PorterDuff.Mode.SRC_IN);
holder.getEncryptionStatusIcon().setImageDrawable(context.getResources().getDrawable(encIcon)); holder.getEncryptionStatusIcon().setImageDrawable(resources.getDrawable(encIcon));
holder.getEncryptionStatusText().setText(encText); holder.getEncryptionStatusText().setText(encText);
holder.getEncryptionStatusText().setTextColor(encColorRes); holder.getEncryptionStatusText().setTextColor(encColorRes);
} }
@@ -577,9 +578,9 @@ public class KeyFormattingUtils {
} }
int sigColorRes = context.getResources().getColor(sigColor); int sigColorRes = resources.getColor(sigColor);
holder.getSignatureStatusIcon().setColorFilter(sigColorRes, PorterDuff.Mode.SRC_IN); holder.getSignatureStatusIcon().setColorFilter(sigColorRes, PorterDuff.Mode.SRC_IN);
holder.getSignatureStatusIcon().setImageDrawable(context.getResources().getDrawable(sigIcon)); holder.getSignatureStatusIcon().setImageDrawable(resources.getDrawable(sigIcon));
holder.getSignatureStatusText().setText(sigText); holder.getSignatureStatusText().setText(sigText);
holder.getSignatureStatusText().setTextColor(sigColorRes); holder.getSignatureStatusText().setTextColor(sigColorRes);

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 B

View File

@@ -24,7 +24,7 @@
android:outAnimation="@anim/fade_out" android:outAnimation="@anim/fade_out"
android:id="@+id/view_animator" android:id="@+id/view_animator"
android:measureAllChildren="false" android:measureAllChildren="false"
custom:initialView="0" custom:initialView="1"
android:minHeight="?listPreferredItemHeightSmall" android:minHeight="?listPreferredItemHeightSmall"
android:animateLayoutChanges="true" android:animateLayoutChanges="true"
> >
@@ -78,14 +78,25 @@
<TextView <TextView
android:id="@+id/result_encryption_text" android:id="@+id/result_encryption_text"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="?android:attr/textAppearanceMedium" android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:text="" android:text=""
tools:text="Encryption status text" /> tools:text="Encryption status text" />
<ImageView
android:id="@+id/context_menu"
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" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
@@ -121,7 +132,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clickable="true" android:clickable="true"
android:background="?android:selectableItemBackground" android:background="?android:selectableItemBackground"
android:orientation="horizontal"> android:orientation="horizontal"
style="?listPreferredItemHeight"
>
<LinearLayout <LinearLayout
android:layout_width="0dp" android:layout_width="0dp"
@@ -184,62 +197,10 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:id="@+id/file" android:id="@+id/file_list"
android:clickable="true" android:orientation="vertical">
android:background="?android:selectableItemBackground"
>
<ImageView <include layout="@layout/decrypt_list_file_item" />
android:id="@+id/thumbnail"
android:layout_gravity="center_vertical"
android:layout_width="36dp"
android:layout_height="36dp"
android:scaleType="center"
android:padding="6dp"
android:src="@drawable/ic_doc_generic_am" />
<LinearLayout
android:orientation="vertical"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:layout_weight="1">
<TextView
android:id="@+id/filename"
android:maxLines="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorSecondary"
android:textAppearance="?android:attr/textAppearanceMedium"
android:ellipsize="end"
android:text=""
tools:text="filename.jpg" />
<TextView
android:id="@+id/filesize"
android:maxLines="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorTertiary"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="12sp"
android:ellipsize="end"
android:text=""
tools:text="14kb" />
</LinearLayout>
<ImageView
android:id="@+id/context_menu"
android:scaleType="center"
android:layout_width="36dip"
android:layout_height="48dip"
android:clickable="true"
android:background="?android:selectableItemBackground"
android:src="@drawable/ic_menu_moreoverflow_normal_holo_light" />
</LinearLayout> </LinearLayout>

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/file"
android:clickable="true"
android:background="?android:selectableItemBackground"
android:minHeight="?listPreferredItemHeight"
>
<ImageView
android:id="@+id/thumbnail"
android:layout_gravity="center_vertical"
android:layout_width="36dp"
android:layout_height="36dp"
android:scaleType="center"
android:padding="6dp"
android:src="@drawable/ic_doc_generic_am" />
<LinearLayout
android:orientation="vertical"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:layout_weight="1">
<TextView
android:id="@+id/filename"
android:maxLines="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorSecondary"
android:textAppearance="?android:attr/textAppearanceMedium"
android:ellipsize="end"
android:text=""
tools:text="filename.jpg" />
<TextView
android:id="@+id/filesize"
android:maxLines="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorTertiary"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="12sp"
android:ellipsize="end"
android:text=""
tools:text="14kb" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/decrypt_open"
android:title="Open with…"
android:icon="@drawable/ic_apps_black_24dp" />
<item
android:id="@+id/decrypt_share"
android:title="@string/btn_share_decrypted_text"
android:icon="@drawable/ic_share_black_24dp" />
<item
android:id="@+id/decrypt_save"
android:title="@string/btn_save"
android:icon="@drawable/ic_save_black_24dp" />
</menu>

View File

@@ -7,18 +7,6 @@
android:icon="@drawable/ic_view_list_grey_24dp" android:icon="@drawable/ic_view_list_grey_24dp"
/> />
<item
android:id="@+id/decrypt_share"
android:title="@string/btn_share_decrypted_text"
android:icon="@drawable/ic_share_grey_24dp"
/>
<item
android:id="@+id/decrypt_save"
android:title="@string/btn_save_file"
android:icon="@drawable/ic_action_encrypt_file_24dp"
/>
<item <item
android:id="@+id/decrypt_delete" android:id="@+id/decrypt_delete"
android:title="@string/btn_delete_original" android:title="@string/btn_delete_original"

View File

@@ -4,7 +4,7 @@
<item <item
android:id="@+id/menu_log_display_export_log" android:id="@+id/menu_log_display_export_log"
android:icon="@drawable/ic_share_black_24dp" android:icon="@drawable/ic_share_white_24dp"
android:title="@string/menu_share_log" android:title="@string/menu_share_log"
app:showAsAction="ifRoom|withText" /> app:showAsAction="ifRoom|withText" />

View File

@@ -1355,6 +1355,27 @@
<string name="msg_lv_fetch_error_format">"Format error!"</string> <string name="msg_lv_fetch_error_format">"Format error!"</string>
<string name="msg_lv_fetch_error_nothing">"Resource not found!"</string> <string name="msg_lv_fetch_error_nothing">"Resource not found!"</string>
<string name="msg_data">"Processing input data"</string>
<string name="msg_data_openpgp">"Attempting to process OpenPGP data"</string>
<string name="msg_data_detached">"Encountered detached signature"</string>
<string name="msg_data_detached_clear">"Clearing earlier, unsigned data!"</string>
<string name="msg_data_detached_sig">"Processing detached signature"</string>
<string name="msg_data_detached_raw">"Processing signed data"</string>
<string name="msg_data_detached_nested">"Skipping nested signed data!"</string>
<string name="msg_data_detached_trailing">"Skipping trailing data after signed part!"</string>
<string name="msg_data_detached_unsupported">"Unsupported type of detached signature!"</string>
<string name="msg_data_error_io">"Error reading input data!"</string>
<string name="msg_data_error_openpgp">"Error processing OpenPGP data!"</string>
<string name="msg_data_mime_error">"Error parsing MIME data!"</string>
<string name="msg_data_mime_filename">"Filename: '%s'"</string>
<string name="msg_data_mime_length">"Content-Length: %s"</string>
<string name="msg_data_mime">"Parsing MIME data structure"</string>
<string name="msg_data_mime_ok">"Finished parsing</string>
<string name="msg_data_mime_none">"No MIME structure found"</string>
<string name="msg_data_mime_part">"Processing MIME part"</string>
<string name="msg_data_mime_type">"Content-Type: %s"</string>
<string name="msg_data_ok">"Data processing successful"</string>
<string name="msg_data_skip_mime">"Skipping MIME parsing"</string>
<string name="msg_acc_saved">"Account saved"</string> <string name="msg_acc_saved">"Account saved"</string>
@@ -1380,6 +1401,11 @@
<string name="msg_keybase_error_specific">"%s"</string> <string name="msg_keybase_error_specific">"%s"</string>
<string name="msg_keybase_error_msg_payload_mismatch">"Decrypted proof post does not match expected value"</string> <string name="msg_keybase_error_msg_payload_mismatch">"Decrypted proof post does not match expected value"</string>
<!-- Messages for Mime parsing operation -->
<string name="msg_mime_parsing_start">"Parsing the MIME structure"</string>
<string name="msg_mime_parsing_error">"MIME parsing failed"</string>
<string name="msg_mime_parsing_success">"MIME parsing successfully!"</string>
<!-- PassphraseCache --> <!-- PassphraseCache -->
<string name="passp_cache_notif_click_to_clear">"Touch to clear passwords."</string> <string name="passp_cache_notif_click_to_clear">"Touch to clear passwords."</string>
<plurals name="passp_cache_notif_n_keys"> <plurals name="passp_cache_notif_n_keys">
@@ -1521,9 +1547,12 @@
<string name="error_loading_keys">"Error loading keys!"</string> <string name="error_loading_keys">"Error loading keys!"</string>
<string name="error_empty_log">"(error, empty log)"</string> <string name="error_empty_log">"(error, empty log)"</string>
<string name="error_reading_text">"Could not read input to decrypt!"</string> <string name="error_reading_text">"Could not read input to decrypt!"</string>
<string name="error_reading_aosp">"Failed reading data, this is a bug in the Android E-Mail client! (Issue #290)"</string>
<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">Unknown filename (click to open)</string>
<string name="filename_unknown_text">Text (click to show)</string> <string name="filename_unknown_text">Text (click to show)</string>
<string name="intent_show">Show Signed/Encrypted Content</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> <string name="view_internal">"View in OpenKeychain"</string>
<string name="error_preparing_data">"Error preparing data!"</string> <string name="error_preparing_data">"Error preparing data!"</string>
<string name="label_clip_title">"Encrypted Data"</string> <string name="label_clip_title">"Encrypted Data"</string>

View File

@@ -0,0 +1,162 @@
/*
* 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.pgp;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.security.Security;
import java.util.ArrayList;
import android.app.Application;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.net.Uri;
import junit.framework.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLog;
import org.spongycastle.jce.provider.BouncyCastleProvider;
import org.sufficientlysecure.keychain.WorkaroundBuildConfig;
import org.sufficientlysecure.keychain.operations.InputDataOperation;
import org.sufficientlysecure.keychain.operations.results.InputDataResult;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider;
import org.sufficientlysecure.keychain.service.InputDataParcel;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.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")
public class InputDataOperationTest {
static PrintStream oldShadowStream;
@BeforeClass
public static void setUpOnce() throws Exception {
Security.insertProviderAt(new BouncyCastleProvider(), 1);
oldShadowStream = ShadowLog.stream;
// ShadowLog.stream = System.out;
}
@Before
public void setUp() {
// don't log verbosely here, we're not here to test imports
ShadowLog.stream = oldShadowStream;
// ok NOW log verbosely!
ShadowLog.stream = System.out;
}
@Test
public void testMimeDecoding () throws Exception {
String mimeMail =
"Content-Type: multipart/mixed; boundary=\"=-26BafqxfXmhVNMbYdoIi\"\n" +
"\n" +
"--=-26BafqxfXmhVNMbYdoIi\n" +
"Content-Type: text/plain\n" +
"Content-Transfer-Encoding: quoted-printable\n" +
"Content-Disposition: attachment; filename=data.txt\n" +
"\n" +
"message part 1\n" +
"\n" +
"--=-26BafqxfXmhVNMbYdoIi\n" +
"Content-Type: text/testvalue\n" +
"Content-Description: Dummy content description\n" +
"\n" +
"message part 2.1\n" +
"message part 2.2\n" +
"\n" +
"--=-26BafqxfXmhVNMbYdoIi--";
ByteArrayOutputStream outStream1 = new ByteArrayOutputStream();
ByteArrayOutputStream outStream2 = new ByteArrayOutputStream();
ContentResolver mockResolver = mock(ContentResolver.class);
// fake openOutputStream first and second
when(mockResolver.openOutputStream(any(Uri.class), eq("w")))
.thenReturn(outStream1, outStream2);
// fake openInputStream
Uri fakeInputUri = Uri.parse("content://fake/1");
when(mockResolver.openInputStream(fakeInputUri)).thenReturn(
new ByteArrayInputStream(mimeMail.getBytes()));
Uri fakeOutputUri1 = Uri.parse("content://fake/out/1");
Uri fakeOutputUri2 = Uri.parse("content://fake/out/2");
when(mockResolver.insert(eq(TemporaryStorageProvider.CONTENT_URI), any(ContentValues.class)))
.thenReturn(fakeOutputUri1, fakeOutputUri2);
// application which returns mockresolver
Application spyApplication = spy(RuntimeEnvironment.application);
when(spyApplication.getContentResolver()).thenReturn(mockResolver);
InputDataOperation op = new InputDataOperation(spyApplication,
new ProviderHelper(RuntimeEnvironment.application), null);
InputDataParcel input = new InputDataParcel(fakeInputUri, null);
InputDataResult result = op.execute(input, new CryptoInputParcel());
// must be successful, no verification, have two output URIs
Assert.assertTrue(result.success());
Assert.assertNull(result.mDecryptVerifyResult);
ArrayList<Uri> outUris = result.getOutputUris();
Assert.assertEquals("must have two output URIs", 2, outUris.size());
Assert.assertEquals("first uri must be the one we provided", fakeOutputUri1, outUris.get(0));
verify(mockResolver).openOutputStream(result.getOutputUris().get(0), "w");
Assert.assertEquals("second uri must be the one we provided", fakeOutputUri2, outUris.get(1));
verify(mockResolver).openOutputStream(result.getOutputUris().get(1), "w");
ContentValues contentValues = new ContentValues();
contentValues.put("name", "data.txt");
contentValues.put("mimetype", "text/plain");
verify(mockResolver).insert(TemporaryStorageProvider.CONTENT_URI, contentValues);
contentValues.put("name", (String) null);
contentValues.put("mimetype", "text/testvalue");
verify(mockResolver).insert(TemporaryStorageProvider.CONTENT_URI, contentValues);
// quoted-printable returns windows style line endings for some reason?
Assert.assertEquals("first part must have expected content",
"message part 1\r\n", new String(outStream1.toByteArray()));
Assert.assertEquals("second part must have expected content",
"message part 2.1\nmessage part 2.2\n", new String(outStream2.toByteArray()));
}
}

View File

@@ -792,9 +792,9 @@ public class PgpEncryptDecryptTest {
Assert.assertArrayEquals("decrypted ciphertext should equal plaintext bytes", Assert.assertArrayEquals("decrypted ciphertext should equal plaintext bytes",
out.toByteArray(), plaindata); out.toByteArray(), plaindata);
Assert.assertEquals("charset should be read correctly", Assert.assertEquals("charset should be read correctly",
"iso-2022-jp", result.getCharset()); "iso-2022-jp", result.getDecryptionMetadata().getCharset());
Assert.assertEquals("decrypted ciphertext should equal plaintext", Assert.assertEquals("decrypted ciphertext should equal plaintext",
new String(out.toByteArray(), result.getCharset()), plaintext); new String(out.toByteArray(), result.getDecryptionMetadata().getCharset()), plaintext);
Assert.assertEquals("decryptionResult should be RESULT_ENCRYPTED", Assert.assertEquals("decryptionResult should be RESULT_ENCRYPTED",
OpenPgpDecryptionResult.RESULT_ENCRYPTED, result.getDecryptionResult().getResult()); OpenPgpDecryptionResult.RESULT_ENCRYPTED, result.getDecryptionResult().getResult());
Assert.assertEquals("signatureResult should be RESULT_NO_SIGNATURE", Assert.assertEquals("signatureResult should be RESULT_NO_SIGNATURE",