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
commit 2ebcc942d4
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_paste 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 editor grey mode_edit 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_add 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 navigation grey chevron_left 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 social black share 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
python copy OpenKeychain communication black vpn_key 24

View file

@ -20,6 +20,7 @@ dependencies {
// http://www.vogella.com/tutorials/Robolectric/article.html
testCompile 'junit:junit:4.12'
testCompile 'org.robolectric:robolectric:3.0'
testCompile 'org.mockito:mockito-core:1.+'
// UI testing with Espresso
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.nispok:snackbar:2.11.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 'com.cocosw:bottomsheet:1.1.1@aar'
// libs as submodules
compile project(':extern:openpgp-api-lib:openpgp-api')
@ -209,9 +213,9 @@ android {
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 {
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
jumboMode = true
javaMaxHeapSize "2g"
@ -221,6 +225,9 @@ android {
exclude 'LICENSE.txt'
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/NOTICE.txt'
exclude 'META-INF/DEPENDENCIES'
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE'
exclude '.readme'
}
}

View file

@ -133,7 +133,7 @@ public class SymmetricTextOperationTests {
hasExtra(equalTo(Intent.EXTRA_INTENT), allOf(
hasAction(Intent.ACTION_VIEW),
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")
))
)).respondWith(new ActivityResult(Activity.RESULT_OK, null));

View file

@ -96,7 +96,7 @@ public class ViewKeyAdvShareTest {
hasType("text/plain"),
hasExtra(is(Intent.EXTRA_TEXT), is("openpgp4fpr:c619d53f7a5f96f391a84ca79d604d2f310716a3")),
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));
onView(withId(R.id.view_key_action_fingerprint_share)).perform(click());
@ -113,7 +113,7 @@ public class ViewKeyAdvShareTest {
hasType("text/plain"),
hasExtra(is(Intent.EXTRA_TEXT), startsWith("----")),
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));
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
https://github.com/open-keychain/open-keychain/issues/290
-->
<!--<data android:mimeType="text/plain" />-->
<data android:mimeType="text/plain" />
</intent-filter>
<!-- DECRYPT_DATA with data Uri -->
<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;
OpenPgpDecryptionResult mDecryptionResult;
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;
@ -96,14 +93,6 @@ public class DecryptVerifyResult extends InputPendingResult {
mDecryptionMetadata = decryptMetadata;
}
public String getCharset () {
return mCharset;
}
public void setCharset(String charset) {
mCharset = charset;
}
public void setOutputBytes(byte[] 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;
}
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,
CryptoInputParcel cryptoInputParcel) {
super(RESULT_PENDING, log);

View file

@ -126,6 +126,13 @@ public abstract class OperationResult implements Parcelable {
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) {
mType = LogType.values()[source.readInt()];
mParameters = (Object[]) source.readSerializable();
@ -818,7 +825,29 @@ public abstract class OperationResult implements Parcelable {
MSG_KEYBASE_ERROR_PAYLOAD_MISMATCH(LogLevel.ERROR,
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_MATCH (LogLevel.DEBUG, R.string.msg_lv_match),
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_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_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 LogLevel mLevel;
@ -896,6 +926,13 @@ public abstract class OperationResult implements Parcelable {
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() {
if (mParcels.size() != 1) {
return null;
@ -974,7 +1011,7 @@ public abstract class OperationResult implements Parcelable {
for (LogEntryParcel entry : this) {
log.append(entry.getPrintableLogEntry(resources, indent)).append("\n");
}
return log.toString().substring(0, log.length() -1); // get rid of extra new line
return log.toString().substring(0, log.length() - 1); // get rid of extra new line
}
}

View file

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

View file

@ -556,12 +556,12 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
originalFilename,
mimeType,
literalData.getModificationTime().getTime(),
originalSize == null ? 0 : originalSize);
originalSize == null ? 0 : originalSize,
charset);
log.add(LogType.MSG_DC_OK_META_ONLY, indent);
DecryptVerifyResult result =
new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log);
result.setCharset(charset);
result.setDecryptionMetadata(metadata);
return result;
}
@ -607,7 +607,7 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
}
metadata = new OpenPgpMetadata(
originalFilename, mimeType, literalData.getModificationTime().getTime(), alreadyWritten);
originalFilename, mimeType, literalData.getModificationTime().getTime(), alreadyWritten, charset);
if (signature != null) {
updateProgress(R.string.progress_verifying_signature, 90, 100);
@ -663,7 +663,6 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
result.setCachedCryptoInputParcel(cryptoInput);
result.setSignatureResult(signatureResultBuilder.build());
result.setCharset(charset);
result.setDecryptionResult(decryptionResultBuilder.build());
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_TIME = "time";
private static final String COLUMN_TYPE = "mimetype";
public static final String CONTENT_AUTHORITY = Constants.TEMPSTORAGE_AUTHORITY;
private static final Uri BASE_URI = Uri.parse("content://" + CONTENT_AUTHORITY);
public static final String AUTHORITY = Constants.TEMPSTORAGE_AUTHORITY;
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY);
private static final int DB_VERSION = 3;
private static File cacheDir;
@ -77,18 +77,18 @@ public class TemporaryStorageProvider extends ContentProvider {
ContentValues contentValues = new ContentValues();
contentValues.put(COLUMN_NAME, targetName);
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) {
ContentValues contentValues = new ContentValues();
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) {
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) {
@ -98,7 +98,7 @@ public class TemporaryStorageProvider extends ContentProvider {
}
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)});
}
@ -163,12 +163,19 @@ public class TemporaryStorageProvider extends ContentProvider {
throw new SecurityException("Listing temporary files is not allowed, only querying single files.");
}
Log.d(Constants.TAG, "being asked for file " + uri);
File file;
try {
file = getFile(uri);
if (file.exists()) {
Log.e(Constants.TAG, "already exists");
}
} catch (FileNotFoundException e) {
Log.e(Constants.TAG, "file not found!");
return null;
}
Cursor fileName = db.getReadableDatabase().query(TABLE_FILES, new String[]{COLUMN_NAME}, COLUMN_ID + "=?",
new String[]{uri.getLastPathSegment()}, null, null, null);
if (fileName != null) {
@ -236,7 +243,7 @@ public class TemporaryStorageProvider extends ContentProvider {
Log.e(Constants.TAG, "File creation failed!");
return null;
}
return Uri.withAppendedPath(BASE_URI, uuid);
return Uri.withAppendedPath(CONTENT_URI, uuid);
}
@Override
@ -274,6 +281,7 @@ public class TemporaryStorageProvider extends ContentProvider {
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
Log.d(Constants.TAG, "openFile");
return openFileHelper(uri, mode);
}

View file

@ -628,15 +628,14 @@ public class OpenPgpService extends RemoteService {
}
}
OpenPgpMetadata metadata = pgpResult.getDecryptionMetadata();
if (data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) >= 4) {
OpenPgpMetadata metadata = pgpResult.getDecryptionMetadata();
if (metadata != null) {
result.putExtra(OpenPgpApi.RESULT_METADATA, metadata);
}
}
String charset = pgpResult.getCharset();
String charset = metadata != null ? metadata.getCharset() : null;
if (charset != null) {
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.ImportOperation;
import org.sufficientlysecure.keychain.operations.KeybaseVerificationOperation;
import org.sufficientlysecure.keychain.operations.InputDataOperation;
import org.sufficientlysecure.keychain.operations.PromoteKeyOperation;
import org.sufficientlysecure.keychain.operations.RevokeOperation;
import org.sufficientlysecure.keychain.operations.SignEncryptOperation;
@ -108,35 +109,29 @@ public class KeychainService extends Service implements Progressable {
// just for brevity
KeychainService outerThis = KeychainService.this;
if (inputParcel instanceof SignEncryptParcel) {
op = new SignEncryptOperation(outerThis, new ProviderHelper(outerThis),
outerThis, mActionCanceled);
op = new SignEncryptOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled);
} else if (inputParcel instanceof PgpDecryptVerifyInputParcel) {
op = new PgpDecryptVerifyOperation(outerThis, new ProviderHelper(outerThis), outerThis);
} else if (inputParcel instanceof SaveKeyringParcel) {
op = new EditKeyOperation(outerThis, new ProviderHelper(outerThis), outerThis,
mActionCanceled);
op = new EditKeyOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled);
} else if (inputParcel instanceof RevokeKeyringParcel) {
op = new RevokeOperation(outerThis, new ProviderHelper(outerThis), outerThis);
} else if (inputParcel instanceof CertifyActionsParcel) {
op = new CertifyOperation(outerThis, new ProviderHelper(outerThis), outerThis,
mActionCanceled);
op = new CertifyOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled);
} else if (inputParcel instanceof DeleteKeyringParcel) {
op = new DeleteOperation(outerThis, new ProviderHelper(outerThis), outerThis);
} else if (inputParcel instanceof PromoteKeyringParcel) {
op = new PromoteKeyOperation(outerThis, new ProviderHelper(outerThis),
outerThis, mActionCanceled);
op = new PromoteKeyOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled);
} else if (inputParcel instanceof ImportKeyringParcel) {
op = new ImportOperation(outerThis, new ProviderHelper(outerThis), outerThis,
mActionCanceled);
op = new ImportOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled);
} else if (inputParcel instanceof ExportKeyringParcel) {
op = new ExportOperation(outerThis, new ProviderHelper(outerThis), outerThis,
mActionCanceled);
op = new ExportOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled);
} else if (inputParcel instanceof ConsolidateInputParcel) {
op = new ConsolidateOperation(outerThis, new ProviderHelper(outerThis),
outerThis);
op = new ConsolidateOperation(outerThis, new ProviderHelper(outerThis), outerThis);
} else if (inputParcel instanceof KeybaseVerificationParcel) {
op = new KeybaseVerificationOperation(outerThis, new ProviderHelper(outerThis),
outerThis);
op = new KeybaseVerificationOperation(outerThis, new ProviderHelper(outerThis), outerThis);
} else if (inputParcel instanceof InputDataParcel) {
op = new InputDataOperation(outerThis, new ProviderHelper(outerThis), outerThis);
} else {
throw new AssertionError("Unrecognized input parcel in KeychainService!");
}

View file

@ -82,6 +82,9 @@ public class DecryptActivity extends BaseActivity {
return;
}
// depending on the data source, we may or may not be able to delete the original file
boolean canDelete = false;
try {
switch (action) {
@ -152,10 +155,21 @@ public class DecryptActivity extends BaseActivity {
}
// for everything else, just work on the intent data
case OpenKeychainIntents.DECRYPT_DATA:
case Intent.ACTION_VIEW:
canDelete = true;
case OpenKeychainIntents.DECRYPT_DATA:
default:
uris.add(intent.getData());
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());
}
}
@ -173,13 +187,17 @@ public class DecryptActivity extends BaseActivity {
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);
OutputStream outStream = getContentResolver().openOutputStream(tempFile);
if (outStream == null) {
return null;
}
// clean up ascii armored message, fixing newlines and stuff
String cleanedText = PgpHelper.getPgpContent(text);
@ -188,14 +206,14 @@ public class DecryptActivity extends BaseActivity {
}
// if cleanup didn't work, just try the raw data
outStream.write(text.getBytes());
outStream.write(cleanedText.getBytes());
outStream.close();
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();

View file

@ -28,6 +28,7 @@ import android.app.Activity;
import android.content.ClipDescription;
import android.content.Context;
import android.content.Intent;
import android.content.pm.LabeledIntent;
import android.content.pm.ResolveInfo;
import android.graphics.Bitmap;
import android.graphics.Point;
@ -36,6 +37,7 @@ import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
@ -44,26 +46,33 @@ import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.ViewGroup;
import android.webkit.MimeTypeMap;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.PopupMenu.OnDismissListener;
import android.widget.PopupMenu.OnMenuItemClickListener;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ViewAnimator;
import com.cocosw.bottomsheet.BottomSheet;
import org.openintents.openpgp.OpenPgpMetadata;
import org.openintents.openpgp.OpenPgpSignatureResult;
import org.sufficientlysecure.keychain.BuildConfig;
import org.sufficientlysecure.keychain.Constants;
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.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider;
// this import NEEDS to be above the ViewModel one, or it won't compile! (as of 06/06/15)
import org.sufficientlysecure.keychain.service.InputDataParcel;
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.DecryptListFragment.ViewHolder.SubViewHolder;
import org.sufficientlysecure.keychain.ui.DecryptListFragment.DecryptFilesAdapter.ViewModel;
import org.sufficientlysecure.keychain.ui.adapter.SpacesItemDecoration;
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
@ -76,34 +85,38 @@ import org.sufficientlysecure.keychain.util.ParcelableHashMap;
public class DecryptListFragment
extends QueueingCryptoOperationFragment<PgpDecryptVerifyInputParcel,DecryptVerifyResult>
extends QueueingCryptoOperationFragment<InputDataParcel,InputDataResult>
implements OnMenuItemClickListener {
public static final String ARG_INPUT_URIS = "input_uris";
public static final String ARG_OUTPUT_URIS = "output_uris";
public static final String ARG_CANCELLED_URIS = "cancelled_uris";
public static final String ARG_RESULTS = "results";
public static final String ARG_CAN_DELETE = "can_delete";
private static final int REQUEST_CODE_OUTPUT = 0x00007007;
public static final String ARG_CURRENT_URI = "current_uri";
private ArrayList<Uri> mInputUris;
private HashMap<Uri, Uri> mOutputUris;
private HashMap<Uri, InputDataResult> mInputDataResults;
private ArrayList<Uri> mPendingInputUris;
private ArrayList<Uri> mCancelledInputUris;
private Uri mCurrentInputUri;
private boolean mCanDelete;
private DecryptFilesAdapter mAdapter;
private Uri mCurrentSaveFileUri;
/**
* 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();
Bundle args = new Bundle();
args.putParcelableArrayList(ARG_INPUT_URIS, uris);
args.putBoolean(ARG_CAN_DELETE, canDelete);
frag.setArguments(args);
return frag;
@ -129,7 +142,7 @@ public class DecryptListFragment
vFilesList.setLayoutManager(new LinearLayoutManager(getActivity()));
vFilesList.setItemAnimator(new DefaultItemAnimator());
mAdapter = new DecryptFilesAdapter(getActivity(), this);
mAdapter = new DecryptFilesAdapter();
vFilesList.setAdapter(mAdapter);
return view;
@ -141,21 +154,22 @@ public class DecryptListFragment
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) {
if (mPendingInputUris.contains(uri)) {
continue;
}
DecryptVerifyResult result = mAdapter.getItemResult(uri);
InputDataResult result = mAdapter.getItemResult(uri);
if (result != null) {
results.put(uri, result);
}
}
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.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> cancelledUris = args.getParcelableArrayList(ARG_CANCELLED_URIS);
ParcelableHashMap<Uri,Uri> outputUris = args.getParcelable(ARG_OUTPUT_URIS);
ParcelableHashMap<Uri,DecryptVerifyResult> results = args.getParcelable(ARG_RESULTS);
ParcelableHashMap<Uri,InputDataResult> results = args.getParcelable(ARG_RESULTS);
Uri currentInputUri = args.getParcelable(ARG_CURRENT_URI);
mCanDelete = args.getBoolean(ARG_CAN_DELETE, false);
displayInputUris(inputUris, currentInputUri, cancelledUris,
outputUris != null ? outputUris.getMap() : null,
results != null ? results.getMap() : null
);
}
private void displayInputUris(ArrayList<Uri> inputUris, Uri currentInputUri,
ArrayList<Uri> cancelledUris, HashMap<Uri,Uri> outputUris,
HashMap<Uri,DecryptVerifyResult> results) {
ArrayList<Uri> cancelledUris, HashMap<Uri,InputDataResult> results) {
mInputUris = inputUris;
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>();
mPendingInputUris = new ArrayList<>();
@ -206,9 +219,8 @@ public class DecryptListFragment
}
if (results != null && results.containsKey(uri)) {
processResult(uri, results.get(uri));
processResult(uri);
} else {
mOutputUris.put(uri, TemporaryStorageProvider.createFile(getActivity()));
mPendingInputUris.add(uri);
}
}
@ -224,9 +236,8 @@ public class DecryptListFragment
case REQUEST_CODE_OUTPUT: {
// This happens after output file was selected, so start our operation
if (resultCode == Activity.RESULT_OK && data != null) {
Uri decryptedFileUri = mOutputUris.get(mCurrentInputUri);
Uri saveUri = data.getData();
saveFile(decryptedFileUri, saveUri);
saveFile(saveUri);
mCurrentInputUri = null;
}
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();
if (activity == null) {
return;
@ -260,21 +301,27 @@ public class DecryptListFragment
}
@Override
public void onQueuedOperationError(DecryptVerifyResult result) {
public void onQueuedOperationError(InputDataResult result) {
final Uri uri = mCurrentInputUri;
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();
}
@Override
public void onQueuedOperationSuccess(DecryptVerifyResult result) {
public void onQueuedOperationSuccess(InputDataResult result) {
Uri uri = mCurrentInputUri;
mCurrentInputUri = null;
processResult(uri, result);
mInputDataResults.put(uri, result);
processResult(uri);
cryptoOperation();
}
@ -298,39 +345,57 @@ 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
protected Drawable doInBackground(Void... params) {
protected Void doInBackground(Void... params) {
InputDataResult result = mInputDataResults.get(uri);
Context context = getActivity();
if (result.getDecryptionMetadata() == null || context == null) {
if (context == null) {
return null;
}
String type = result.getDecryptionMetadata().getMimeType();
Uri outputUri = mOutputUris.get(uri);
if (type == null || outputUri == null) {
return null;
}
for (int i = 0; i < result.getOutputUris().size(); i++) {
TemporaryStorageProvider.setMimeType(context, outputUri, type);
Uri outputUri = result.getOutputUris().get(i);
if (mIconCache.containsKey(outputUri)) {
continue;
}
if (ClipDescription.compareMimeTypes(type, "image/*")) {
int px = FormattingUtils.dpToPx(context, 48);
Bitmap bitmap = FileHelper.getThumbnail(context, outputUri, new Point(px, px));
return new BitmapDrawable(context.getResources(), bitmap);
}
OpenPgpMetadata metadata = result.mMetadata.get(i);
String type = metadata.getMimeType();
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(outputUri, type);
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);
Bitmap bitmap = FileHelper.getThumbnail(context, outputUri, new Point(px, px));
icon = new BitmapDrawable(context.getResources(), bitmap);
} else {
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(outputUri, type);
final List<ResolveInfo> matches =
context.getPackageManager().queryIntentActivities(intent, 0);
// noinspection LoopStatementThatDoesntLoop
for (ResolveInfo match : matches) {
icon = match.loadIcon(getActivity().getPackageManager());
break;
}
}
if (icon != null) {
mIconCache.put(outputUri, icon);
}
final List<ResolveInfo> matches =
context.getPackageManager().queryIntentActivities(intent, 0);
//noinspection LoopStatementThatDoesntLoop
for (ResolveInfo match : matches) {
return match.loadIcon(getActivity().getPackageManager());
}
return null;
@ -338,49 +403,14 @@ public class DecryptListFragment
}
@Override
protected void onPostExecute(Drawable icon) {
processResult(uri, result, icon);
protected void onPostExecute(Void v) {
InputDataResult result = mInputDataResults.get(uri);
mAdapter.addResult(uri, result);
}
}.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) {
// 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();
if (activity == null || mCurrentInputUri != null) {
if (activity == null) {
return;
}
final Uri outputUri = mOutputUris.get(uri);
final DecryptVerifyResult result = mAdapter.getItemResult(uri);
if (outputUri == null || result == null) {
new BottomSheet.Builder(activity).sheet(R.menu.decrypt_bottom_sheet).listener(new MenuItem.OnMenuItemClickListener() {
@Override
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;
}
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
// the EXTRA_TEXT extra ourselves, and display a chooser which includes
@ -418,12 +470,14 @@ public class DecryptListFragment
if (share) {
try {
String plaintext = FileHelper.readTextFromUri(activity, outputUri, result.getCharset());
String plaintext = FileHelper.readTextFromUri(activity, outputUri, null);
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType(metadata.getMimeType());
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, plaintext);
startActivity(intent);
Intent chooserIntent = Intent.createChooser(intent, getString(R.string.intent_share));
startActivity(chooserIntent);
} catch (IOException e) {
Notify.create(activity, R.string.error_preparing_data, Style.ERROR).show();
@ -432,11 +486,34 @@ public class DecryptListFragment
return;
}
Intent intent = new Intent(activity, DisplayTextActivity.class);
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(outputUri, metadata.getMimeType());
intent.putExtra(DisplayTextActivity.EXTRA_METADATA, result);
activity.startActivity(intent);
intent.setDataAndType(outputUri, "text/plain");
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 {
@ -454,13 +531,13 @@ public class DecryptListFragment
Intent chooserIntent = Intent.createChooser(intent, getString(R.string.intent_show));
chooserIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
activity.startActivity(chooserIntent);
startActivity(chooserIntent);
}
}
@Override
public PgpDecryptVerifyInputParcel createOperationInput() {
public InputDataParcel createOperationInput() {
if (mCurrentInputUri == null) {
if (mPendingInputUris.isEmpty()) {
@ -471,11 +548,11 @@ public class DecryptListFragment
mCurrentInputUri = mPendingInputUris.remove(0);
}
Uri currentOutputUri = mOutputUris.get(mCurrentInputUri);
Log.d(Constants.TAG, "mInputUri=" + mCurrentInputUri + ", mOutputUri=" + currentOutputUri);
Log.d(Constants.TAG, "mInputUri=" + mCurrentInputUri);
return new PgpDecryptVerifyInputParcel(mCurrentInputUri, currentOutputUri)
PgpDecryptVerifyInputParcel decryptInput = new PgpDecryptVerifyInputParcel()
.setAllowSymmetricDecryption(true);
return new InputDataParcel(mCurrentInputUri, decryptInput);
}
@ -496,25 +573,12 @@ public class DecryptListFragment
}
ViewModel model = mAdapter.mMenuClickedModel;
DecryptVerifyResult result = model.mResult;
switch (menuItem.getItemId()) {
case R.id.view_log:
Intent intent = new Intent(activity, LogDisplayActivity.class);
intent.putExtra(LogDisplayFragment.EXTRA_RESULT, result);
intent.putExtra(LogDisplayFragment.EXTRA_RESULT, model.mResult);
activity.startActivity(intent);
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:
deleteFile(activity, model.mInputUri);
return true;
@ -524,6 +588,9 @@ public class DecryptListFragment
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())) {
File file = new File(uri.getPath());
if (file.delete()) {
@ -553,46 +620,29 @@ public class DecryptListFragment
}
public static class DecryptFilesAdapter extends RecyclerView.Adapter<ViewHolder> {
private Context mContext;
public class DecryptFilesAdapter extends RecyclerView.Adapter<ViewHolder> {
private ArrayList<ViewModel> mDataset;
private OnMenuItemClickListener mMenuItemClickListener;
private ViewModel mMenuClickedModel;
public class ViewModel {
Context mContext;
Uri mInputUri;
DecryptVerifyResult mResult;
Drawable mIcon;
OnClickListener mOnFileClickListener;
OnClickListener mOnKeyClickListener;
InputDataResult mResult;
int mProgress, mMax;
String mProgressMsg;
OnClickListener mCancelled;
ViewModel(Context context, Uri uri) {
mContext = context;
ViewModel(Uri uri) {
mInputUri = uri;
mProgress = 0;
mMax = 100;
mCancelled = null;
}
void addResult(DecryptVerifyResult result) {
void addResult(InputDataResult result) {
mResult = result;
}
void addIcon(Drawable icon) {
mIcon = icon;
}
void setOnClickListeners(OnClickListener onFileClick, OnClickListener onKeyClick) {
mOnFileClickListener = onFileClick;
mOnKeyClickListener = onKeyClick;
}
boolean hasResult() {
return mResult != null;
}
@ -636,9 +686,7 @@ public class DecryptListFragment
}
// Provide a suitable constructor (depends on the kind of dataset)
public DecryptFilesAdapter(Context context, OnMenuItemClickListener menuItemClickListener) {
mContext = context;
mMenuItemClickListener = menuItemClickListener;
public DecryptFilesAdapter() {
mDataset = new ArrayList<>();
}
@ -701,51 +749,103 @@ public class DecryptListFragment
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++) {
String filename;
if (metadata == null) {
filename = mContext.getString(R.string.filename_unknown);
} else if (TextUtils.isEmpty(metadata.getFilename())) {
filename = mContext.getString("text/plain".equals(metadata.getMimeType())
? R.string.filename_unknown_text : R.string.filename_unknown);
} else {
filename = metadata.getFilename();
}
holder.vFilename.setText(filename);
Uri outputUri = model.mResult.getOutputUris().get(i);
OpenPgpMetadata metadata = model.mResult.mMetadata.get(i);
SubViewHolder fileHolder = holder.mFileHolderList.get(i);
String filename;
if (metadata == null) {
filename = getString(R.string.filename_unknown);
} else if (TextUtils.isEmpty(metadata.getFilename())) {
filename = getString("text/plain".equals(metadata.getMimeType())
? R.string.filename_unknown_text : R.string.filename_unknown);
} else {
filename = metadata.getFilename();
}
fileHolder.vFilename.setText(filename);
long size = metadata == null ? 0 : metadata.getOriginalSize();
if (size == -1 || size == 0) {
fileHolder.vFilesize.setText("");
} else {
fileHolder.vFilesize.setText(FileHelper.readableFileSize(size));
}
if (mIconCache.containsKey(outputUri)) {
fileHolder.vThumbnail.setImageDrawable(mIconCache.get(outputUri));
} else {
fileHolder.vThumbnail.setImageResource(R.drawable.ic_doc_generic_am);
}
// save index closure-style :)
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);
}
}
});
long size = metadata == null ? 0 : metadata.getOriginalSize();
if (size == -1 || size == 0) {
holder.vFilesize.setText("");
} else {
holder.vFilesize.setText(FileHelper.readableFileSize(size));
}
if (model.mIcon != null) {
holder.vThumbnail.setImageDrawable(model.mIcon);
} else {
holder.vThumbnail.setImageResource(R.drawable.ic_doc_generic_am);
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.vFile.setOnClickListener(model.mOnFileClickListener);
holder.vSignatureLayout.setOnClickListener(model.mOnKeyClickListener);
holder.vContextMenu.setTag(model);
holder.vContextMenu.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
Activity activity = getActivity();
if (activity == null) {
return;
}
mMenuClickedModel = model;
PopupMenu menu = new PopupMenu(mContext, view);
PopupMenu menu = new PopupMenu(activity, view);
menu.inflate(R.menu.decrypt_item_context_menu);
menu.setOnMenuItemClickListener(mMenuItemClickListener);
menu.setOnMenuItemClickListener(DecryptListFragment.this);
menu.setOnDismissListener(new OnDismissListener() {
@Override
public void onDismiss(PopupMenu popupMenu) {
mMenuClickedModel = null;
}
});
menu.getMenu().findItem(R.id.decrypt_delete).setEnabled(mCanDelete);
menu.show();
}
});
@ -761,9 +861,13 @@ public class DecryptListFragment
holder.vErrorViewLog.setOnClickListener(new OnClickListener() {
@Override
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);
mContext.startActivity(intent);
activity.startActivity(intent);
}
});
@ -775,8 +879,8 @@ public class DecryptListFragment
return mDataset.size();
}
public DecryptVerifyResult getItemResult(Uri uri) {
ViewModel model = new ViewModel(mContext, uri);
public InputDataResult getItemResult(Uri uri) {
ViewModel model = new ViewModel(uri);
int pos = mDataset.indexOf(model);
if (pos == -1) {
return null;
@ -787,37 +891,32 @@ public class DecryptListFragment
}
public void add(Uri uri) {
ViewModel newModel = new ViewModel(mContext, uri);
ViewModel newModel = new ViewModel(uri);
mDataset.add(newModel);
notifyItemInserted(mDataset.size());
}
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);
mDataset.get(pos).setProgress(progress, max, msg);
notifyItemChanged(pos);
}
public void setCancelled(Uri uri, OnClickListener retryListener) {
ViewModel newModel = new ViewModel(mContext, uri);
ViewModel newModel = new ViewModel(uri);
int pos = mDataset.indexOf(newModel);
mDataset.get(pos).setCancelled(retryListener);
notifyItemChanged(pos);
}
public void addResult(Uri uri, DecryptVerifyResult result, Drawable icon,
OnClickListener onFileClick, OnClickListener onKeyClick) {
public void addResult(Uri uri, InputDataResult result) {
ViewModel model = new ViewModel(mContext, uri);
ViewModel model = new ViewModel(uri);
int pos = mDataset.indexOf(model);
model = mDataset.get(pos);
model.addResult(result);
if (icon != null) {
model.addIcon(icon);
}
model.setOnClickListeners(onFileClick, onKeyClick);
notifyItemChanged(pos);
}
@ -834,11 +933,6 @@ public class DecryptListFragment
public ProgressBar vProgress;
public TextView vProgressMsg;
public View vFile;
public TextView vFilename;
public TextView vFilesize;
public ImageView vThumbnail;
public ImageView vEncStatusIcon;
public TextView vEncStatusText;
@ -855,6 +949,25 @@ public class DecryptListFragment
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) {
super(itemView);
@ -863,11 +976,6 @@ public class DecryptListFragment
vProgress = (ProgressBar) itemView.findViewById(R.id.progress);
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);
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);
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);
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
public ImageView getEncryptionStatusIcon() {
return vEncStatusIcon;

View file

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

View file

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

View file

@ -19,6 +19,7 @@
package org.sufficientlysecure.keychain.ui.util;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.text.Spannable;
@ -446,7 +447,7 @@ public class KeyFormattingUtils {
}
@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()) {
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().setImageDrawable(context.getResources().getDrawable(encIcon));
holder.getEncryptionStatusIcon().setImageDrawable(resources.getDrawable(encIcon));
holder.getEncryptionStatusText().setText(encText);
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().setImageDrawable(context.getResources().getDrawable(sigIcon));
holder.getSignatureStatusIcon().setImageDrawable(resources.getDrawable(sigIcon));
holder.getSignatureStatusText().setText(sigText);
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:id="@+id/view_animator"
android:measureAllChildren="false"
custom:initialView="0"
custom:initialView="1"
android:minHeight="?listPreferredItemHeightSmall"
android:animateLayoutChanges="true"
>
@ -78,14 +78,25 @@
<TextView
android:id="@+id/result_encryption_text"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android: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
@ -121,7 +132,9 @@
android:layout_height="wrap_content"
android:clickable="true"
android:background="?android:selectableItemBackground"
android:orientation="horizontal">
android:orientation="horizontal"
style="?listPreferredItemHeight"
>
<LinearLayout
android:layout_width="0dp"
@ -184,62 +197,10 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/file"
android:clickable="true"
android:background="?android:selectableItemBackground"
>
android:id="@+id/file_list"
android:orientation="vertical">
<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>
<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" />
<include layout="@layout/decrypt_list_file_item" />
</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"
/>
<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
android:id="@+id/decrypt_delete"
android:title="@string/btn_delete_original"

View file

@ -4,7 +4,7 @@
<item
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"
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_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>
@ -1380,6 +1401,11 @@
<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>
<!-- 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 -->
<string name="passp_cache_notif_click_to_clear">"Touch to clear passwords."</string>
<plurals name="passp_cache_notif_n_keys">
@ -1521,9 +1547,12 @@
<string name="error_loading_keys">"Error loading keys!"</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_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_text">Text (click to show)</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="error_preparing_data">"Error preparing 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",
out.toByteArray(), plaindata);
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",
new String(out.toByteArray(), result.getCharset()), plaintext);
new String(out.toByteArray(), result.getDecryptionMetadata().getCharset()), plaintext);
Assert.assertEquals("decryptionResult should be RESULT_ENCRYPTED",
OpenPgpDecryptionResult.RESULT_ENCRYPTED, result.getDecryptionResult().getResult());
Assert.assertEquals("signatureResult should be RESULT_NO_SIGNATURE",

@ -1 +1 @@
Subproject commit 13492ba19fcc1767f5589227b8fa0a9c845696d4
Subproject commit 0ba25696981a4c4d5aef01e4a1d683c8adf7522a