From 3cd54581c33b20a9bfa55f767b245fc6e56e83ef Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Tue, 15 Sep 2015 03:02:05 +0200 Subject: mime: create more general InputDataOperation, which for now and does basic mime parsing --- OpenKeychain/build.gradle | 1 + .../keychain/ui/SymmetricTextOperationTests.java | 2 +- .../keychain/ui/ViewKeyAdvShareTest.java | 4 +- .../keychain/operations/InputDataOperation.java | 173 ++++++++++ .../keychain/operations/MimeParsingOperation.java | 360 --------------------- .../operations/results/InputDataResult.java | 71 ++++ .../operations/results/InputPendingResult.java | 9 + .../operations/results/MimeParsingResult.java | 65 ---- .../keychain/pgp/PgpDecryptVerifyInputParcel.java | 10 + .../provider/TemporaryStorageProvider.java | 22 +- .../keychain/service/InputDataParcel.java | 80 +++++ .../keychain/service/KeychainService.java | 31 +- .../keychain/service/MimeParsingParcel.java | 73 ----- .../keychain/ui/DecryptListFragment.java | 3 - .../keychain/pgp/InputDataOperationTest.java | 166 ++++++++++ 15 files changed, 539 insertions(+), 531 deletions(-) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java delete mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/MimeParsingOperation.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/InputDataResult.java delete mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/MimeParsingResult.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/InputDataParcel.java delete mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/MimeParsingParcel.java create mode 100644 OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/InputDataOperationTest.java diff --git a/OpenKeychain/build.gradle b/OpenKeychain/build.gradle index ab640b1ca..c8b095eac 100644 --- a/OpenKeychain/build.gradle +++ b/OpenKeychain/build.gradle @@ -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' diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/SymmetricTextOperationTests.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/SymmetricTextOperationTests.java index 3a34f15be..498df7299 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/SymmetricTextOperationTests.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/SymmetricTextOperationTests.java @@ -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)); diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareTest.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareTest.java index 1e6a3f69e..63c7dc6de 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareTest.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareTest.java @@ -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()); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java new file mode 100644 index 000000000..030c0a285 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2015 Dominik Schürmann + * + * 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 . + */ + +package org.sufficientlysecure.keychain.operations; + + +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.FieldParser; +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.sufficientlysecure.keychain.Constants; +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; +import org.sufficientlysecure.keychain.util.Log; + + +/** This operation deals with input data, trying to determine its type as it goes. */ +public class InputDataOperation extends BaseOperation { + + final private byte[] buf = new byte[256]; + + public InputDataOperation(Context context, ProviderHelper providerHelper, Progressable progressable) { + super(context, providerHelper, progressable); + } + + @NonNull + @Override + public InputDataResult execute(InputDataParcel input, + CryptoInputParcel cryptoInput) { + + final OperationLog log = new OperationLog(); + + log.add(LogType.MSG_MIME_PARSING, 0); + + Uri currentUri; + + PgpDecryptVerifyInputParcel decryptInput = input.getDecryptInput(); + if (decryptInput != null) { + + PgpDecryptVerifyOperation op = + new PgpDecryptVerifyOperation(mContext, mProviderHelper, mProgressable); + + decryptInput.setInputUri(input.getInputUri()); + + currentUri = TemporaryStorageProvider.createFile(mContext); + decryptInput.setOutputUri(currentUri); + + DecryptVerifyResult result = op.execute(decryptInput, cryptoInput); + if (result.isPending()) { + return new InputDataResult(log, result); + } + + } else { + currentUri = input.getInputUri(); + } + + // If we aren't supposed to attempt mime decode, we are done here + if (!input.getMimeDecode()) { + + ArrayList uris = new ArrayList<>(); + uris.add(currentUri); + return new InputDataResult(InputDataResult.RESULT_OK, log, uris); + + } + + try { + InputStream in = mContext.getContentResolver().openInputStream(currentUri); + + MimeStreamParser parser = new MimeStreamParser((MimeConfig) null); + + final ArrayList outputUris = new ArrayList<>(); + + parser.setContentDecoding(true); + parser.setRecurse(); + parser.setContentHandler(new AbstractContentHandler() { + String mFilename; + + @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(); + } + } + + @Override + public void body(BodyDescriptor bd, InputStream is) throws MimeException, IOException { + + // log.add(LogType.MSG_MIME_PART, 0, bd.getMimeType()); + + Uri uri = TemporaryStorageProvider.createFile(mContext, mFilename, bd.getMimeType()); + OutputStream out = mContext.getContentResolver().openOutputStream(uri, "w"); + + if (out == null) { + Log.e(Constants.TAG, "error!"); + return; + } + + int len; + while ( (len = is.read(buf)) > 0) { + out.write(buf, 0, len); + } + + out.close(); + outputUris.add(uri); + + } + }); + + parser.parse(in); + + log.add(LogType.MSG_MIME_PARSING_SUCCESS, 1); + + return new InputDataResult(InputDataResult.RESULT_OK, log, outputUris); + + } catch (FileNotFoundException e) { + e.printStackTrace(); + return new InputDataResult(InputDataResult.RESULT_ERROR, log, null); + } catch (MimeException e) { + e.printStackTrace(); + return new InputDataResult(InputDataResult.RESULT_ERROR, log, null); + } catch (IOException e) { + e.printStackTrace(); + return new InputDataResult(InputDataResult.RESULT_ERROR, log, null); + } + + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/MimeParsingOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/MimeParsingOperation.java deleted file mode 100644 index c7ebbf5fd..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/MimeParsingOperation.java +++ /dev/null @@ -1,360 +0,0 @@ -/* - * Copyright (C) 2015 Dominik Schürmann - * - * 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 . - */ - -package org.sufficientlysecure.keychain.operations; - -import android.content.Context; -import android.net.Uri; -import android.support.annotation.NonNull; - -import org.apache.james.mime4j.dom.BinaryBody; -import org.apache.james.mime4j.dom.Body; -import org.apache.james.mime4j.dom.Entity; -import org.apache.james.mime4j.dom.Message; -import org.apache.james.mime4j.dom.MessageBuilder; -import org.apache.james.mime4j.dom.Multipart; -import org.apache.james.mime4j.dom.TextBody; -import org.apache.james.mime4j.dom.address.Mailbox; -import org.apache.james.mime4j.dom.address.MailboxList; -import org.apache.james.mime4j.dom.field.AddressListField; -import org.apache.james.mime4j.dom.field.ContentTypeField; -import org.apache.james.mime4j.dom.field.DateTimeField; -import org.apache.james.mime4j.dom.field.UnstructuredField; -import org.apache.james.mime4j.field.address.AddressFormatter; -import org.apache.james.mime4j.message.BodyPart; -import org.apache.james.mime4j.message.DefaultMessageBuilder; -import org.apache.james.mime4j.message.MessageImpl; -import org.apache.james.mime4j.stream.Field; -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.operations.results.MimeParsingResult; -import org.sufficientlysecure.keychain.operations.results.OperationResult; -import org.sufficientlysecure.keychain.pgp.Progressable; -import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; -import org.sufficientlysecure.keychain.service.MimeParsingParcel; -import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; -import org.sufficientlysecure.keychain.util.Log; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.Reader; -import java.util.ArrayList; -import java.util.Date; -import java.util.Map; - -public class MimeParsingOperation extends BaseOperation { - - public ArrayList mTempUris; - - public MimeParsingOperation(Context context, ProviderHelper providerHelper, Progressable progressable) { - super(context, providerHelper, progressable); - } - - @NonNull - @Override - public MimeParsingResult execute(MimeParsingParcel parcel, - CryptoInputParcel cryptoInputParcel) { - OperationResult.OperationLog log = new OperationResult.OperationLog(); - - log.add(OperationResult.LogType.MSG_MIME_PARSING, 0); - - mTempUris = new ArrayList<>(); - - try { - InputStream in = mContext.getContentResolver().openInputStream(parcel.getInputUri()); - - final MessageBuilder builder = new DefaultMessageBuilder(); - final Message message = builder.parseMessage(in); - - SimpleTreeNode root = createNode(message); - - traverseTree(root); - - log.add(OperationResult.LogType.MSG_MIME_PARSING_SUCCESS, 1); - - } catch (Exception e) { - Log.e(Constants.TAG, "Mime parsing error", e); - log.add(OperationResult.LogType.MSG_MIME_PARSING_ERROR, 1); - } - - return new MimeParsingResult(MimeParsingResult.RESULT_OK, log, - mTempUris); - } - - private void traverseTree(SimpleTreeNode node) { - if (node.isLeaf()) { - parseAndSaveAsUris(node); - return; - } - - for (SimpleTreeNode child : node.children) { - traverseTree(child); - } - } - - - /** - * Wraps an Object and associates it with a text. All message parts - * (headers, bodies, multiparts, body parts) will be wrapped in - * ObjectWrapper instances before they are added to the JTree instance. - */ - public static class ObjectWrapper { - private String text = ""; - private Object object = null; - - public ObjectWrapper(String text, Object object) { - this.text = text; - this.object = object; - } - - @Override - public String toString() { - return text; - } - - public Object getObject() { - return object; - } - } - -// /** -// * Create a node given a Multipart body. -// * Add the Preamble, all Body parts and the Epilogue to the node. -// * -// * @return the root node of the tree. -// */ -// private DefaultMutableTreeNode createNode(Header header) { -// DefaultMutableTreeNode node = new DefaultMutableTreeNode( -// new ObjectWrapper("Header", header)); -// -// for (Field field : header.getFields()) { -// String name = field.getName(); -// -// node.add(new DefaultMutableTreeNode(new ObjectWrapper(name, field))); -// } -// -// return node; -// } - - /** - * Create a node given a Multipart body. - * Add the Preamble, all Body parts and the Epilogue to the node. - * - * @param multipart the Multipart. - * @return the root node of the tree. - */ - private SimpleTreeNode createNode(Multipart multipart) { - SimpleTreeNode node = new SimpleTreeNode( - new ObjectWrapper("Multipart", multipart)); - -// node.add(new DefaultMutableTreeNode( -// new ObjectWrapper("Preamble", multipart.getPreamble()))); - for (Entity part : multipart.getBodyParts()) { - node.add(createNode(part)); - } -// node.add(new DefaultMutableTreeNode( -// new ObjectWrapper("Epilogue", multipart.getEpilogue()))); - - return node; - } - - /** - * Creates the tree nodes given a MIME entity (either a Message or - * a BodyPart). - * - * @param entity the entity. - * @return the root node of the tree displaying the specified entity and - * its children. - */ - private SimpleTreeNode createNode(Entity entity) { - - /* - * Create the root node for the entity. It's either a - * Message or a Body part. - */ - String type = "Message"; - if (entity instanceof BodyPart) { - type = "Body part"; - } - SimpleTreeNode node = new SimpleTreeNode( - new ObjectWrapper(type, entity)); - - /* - * Add the node encapsulating the entity Header. - */ -// node.add(createNode(entity.getHeader())); - - Body body = entity.getBody(); - - if (body instanceof Multipart) { - /* - * The body of the entity is a Multipart. - */ - - node.add(createNode((Multipart) body)); - } else if (body instanceof MessageImpl) { - /* - * The body is another Message. - */ - - node.add(createNode((MessageImpl) body)); - - } else { - /* - * Discrete Body (either of type TextBody or BinaryBody). - */ - type = "Text body"; - if (body instanceof BinaryBody) { - type = "Binary body"; - } - - type += " (" + entity.getMimeType() + ")"; - node.add(new SimpleTreeNode(new ObjectWrapper(type, body))); - - } - - return node; - } - - public void parseAndSaveAsUris(SimpleTreeNode node) { - Object o = ((ObjectWrapper) node.getUserObject()).getObject(); - - if (o instanceof TextBody) { - /* - * A text body. Display its contents. - */ - TextBody body = (TextBody) o; - StringBuilder sb = new StringBuilder(); - try { - Reader r = body.getReader(); - int c; - while ((c = r.read()) != -1) { - sb.append((char) c); - } - } catch (IOException ex) { - ex.printStackTrace(); - } - Log.d(Constants.TAG, "text: " + sb.toString()); -// textView.setText(sb.toString()); - - Uri tempUri = null; - try { - tempUri = TemporaryStorageProvider.createFile(mContext, "text", "text/plain"); - OutputStream outStream = mContext.getContentResolver().openOutputStream(tempUri); - body.writeTo(outStream); - outStream.close(); - } catch (IOException e) { - Log.e(Constants.TAG, "error mime parsing", e); - } - - mTempUris.add(tempUri); - - } else if (o instanceof BinaryBody) { - /* - * A binary body. Display its MIME type and length in bytes. - */ - BinaryBody body = (BinaryBody) o; - int size = 0; - try { - InputStream is = body.getInputStream(); - while ((is.read()) != -1) { - size++; - } - } catch (IOException ex) { - ex.printStackTrace(); - } - Log.d(Constants.TAG, "Binary body\n" - + "MIME type: " - + body.getParent().getMimeType() + "\n" - + "Size of decoded data: " + size + " bytes"); - - } else if (o instanceof ContentTypeField) { - /* - * Content-Type field. - */ - ContentTypeField field = (ContentTypeField) o; - StringBuilder sb = new StringBuilder(); - sb.append("MIME type: ").append(field.getMimeType()).append("\n"); - for (Map.Entry entry : field.getParameters().entrySet()) { - sb.append(entry.getKey()).append(" = ").append(entry.getValue()).append("\n"); - } - Log.d(Constants.TAG, sb.toString()); - - } else if (o instanceof AddressListField) { - /* - * An address field (From, To, Cc, etc) - */ - AddressListField field = (AddressListField) o; - MailboxList list = field.getAddressList().flatten(); - StringBuilder sb = new StringBuilder(); - for (Mailbox mailbox : list) { - sb.append(AddressFormatter.DEFAULT.format(mailbox, false)).append("\n"); - } - Log.d(Constants.TAG, sb.toString()); - - } else if (o instanceof DateTimeField) { - Date date = ((DateTimeField) o).getDate(); - Log.d(Constants.TAG, date.toString()); - } else if (o instanceof UnstructuredField) { - Log.d(Constants.TAG, ((UnstructuredField) o).getValue()); - } else if (o instanceof Field) { - Log.d(Constants.TAG, ((Field) o).getBody()); - } else { - /* - * The Object should be a Header or a String containing a - * Preamble or Epilogue. - */ - Log.d(Constants.TAG, o.toString()); - } - } - - public class SimpleTreeNode { - private SimpleTreeNode parent; - private Object userObject; - private ArrayList children; - - protected SimpleTreeNode(Object userObject) { - this.parent = null; - this.userObject = userObject; - this.children = new ArrayList<>(); - } - - protected Object getUserObject() { - return userObject; - } - - protected void setUserObject(Object userObject) { - this.userObject = userObject; - } - - public void add(SimpleTreeNode newChild) { - newChild.parent = this; - children.add(newChild); - } - - public SimpleTreeNode getParent() { - return parent; - } - - public boolean isLeaf() { - return children.isEmpty(); - } - - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/InputDataResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/InputDataResult.java new file mode 100644 index 000000000..908636ca7 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/InputDataResult.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2015 Dominik Schürmann + * + * 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 . + */ + +package org.sufficientlysecure.keychain.operations.results; + +import android.net.Uri; +import android.os.Parcel; + +import java.util.ArrayList; + +public class InputDataResult extends InputPendingResult { + + public final ArrayList mOutputUris; + public DecryptVerifyResult mDecryptVerifyResult; + + public InputDataResult(OperationLog log, InputPendingResult result) { + super(log, result); + mOutputUris = null; + } + + public InputDataResult(int result, OperationLog log, ArrayList temporaryUris) { + super(result, log); + mOutputUris = temporaryUris; + } + + protected InputDataResult(Parcel in) { + super(in); + mOutputUris = in.createTypedArrayList(Uri.CREATOR); + } + + public ArrayList getOutputUris() { + return mOutputUris; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeTypedList(mOutputUris); + } + + public static final Creator CREATOR = new Creator() { + @Override + public InputDataResult createFromParcel(Parcel in) { + return new InputDataResult(in); + } + + @Override + public InputDataResult[] newArray(int size) { + return new InputDataResult[size]; + } + }; +} \ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/InputPendingResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/InputPendingResult.java index d767382ae..0a8c1f653 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/InputPendingResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/InputPendingResult.java @@ -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); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/MimeParsingResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/MimeParsingResult.java deleted file mode 100644 index 05f5125cb..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/MimeParsingResult.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2015 Dominik Schürmann - * - * 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 . - */ - -package org.sufficientlysecure.keychain.operations.results; - -import android.net.Uri; -import android.os.Parcel; - -import java.util.ArrayList; - -public class MimeParsingResult extends OperationResult { - - public final ArrayList mTemporaryUris; - - public ArrayList getTemporaryUris() { - return mTemporaryUris; - } - - public MimeParsingResult(int result, OperationLog log, ArrayList temporaryUris) { - super(result, log); - mTemporaryUris = temporaryUris; - } - - protected MimeParsingResult(Parcel in) { - super(in); - mTemporaryUris = in.createTypedArrayList(Uri.CREATOR); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - super.writeToParcel(dest, flags); - dest.writeTypedList(mTemporaryUris); - } - - public static final Creator CREATOR = new Creator() { - @Override - public MimeParsingResult createFromParcel(Parcel in) { - return new MimeParsingResult(in); - } - - @Override - public MimeParsingResult[] newArray(int size) { - return new MimeParsingResult[size]; - } - }; -} \ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyInputParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyInputParcel.java index a6d65688c..d56c24f91 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyInputParcel.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyInputParcel.java @@ -86,10 +86,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; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java index 7e9b24989..67f2c36bc 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java @@ -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); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/InputDataParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/InputDataParcel.java new file mode 100644 index 000000000..807577001 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/InputDataParcel.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2015 Dominik Schürmann + * + * 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 . + */ + +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; + } + + 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 CREATOR = new Creator() { + public InputDataParcel createFromParcel(final Parcel source) { + return new InputDataParcel(source); + } + + public InputDataParcel[] newArray(final int size) { + return new InputDataParcel[size]; + } + }; + +} + diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java index d2128cd77..c7ac92eef 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java @@ -36,7 +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.MimeParsingOperation; +import org.sufficientlysecure.keychain.operations.InputDataOperation; import org.sufficientlysecure.keychain.operations.PromoteKeyOperation; import org.sufficientlysecure.keychain.operations.RevokeOperation; import org.sufficientlysecure.keychain.operations.SignEncryptOperation; @@ -109,38 +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); - } else if (inputParcel instanceof MimeParsingParcel) { - op = new MimeParsingOperation(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!"); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/MimeParsingParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/MimeParsingParcel.java deleted file mode 100644 index ccc817c21..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/MimeParsingParcel.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (C) 2015 Dominik Schürmann - * - * 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 . - */ - -package org.sufficientlysecure.keychain.service; - -import android.net.Uri; -import android.os.Parcel; -import android.os.Parcelable; - -public class MimeParsingParcel implements Parcelable { - - private Uri mInputUri; - private Uri mOutputUri; - - public MimeParsingParcel() { - } - - public MimeParsingParcel(Uri inputUri, Uri outputUri) { - mInputUri = inputUri; - mOutputUri = outputUri; - } - - MimeParsingParcel(Parcel source) { - // we do all of those here, so the PgpSignEncryptInput class doesn't have to be parcelable - mInputUri = source.readParcelable(getClass().getClassLoader()); - mOutputUri = source.readParcelable(getClass().getClassLoader()); - } - - public Uri getInputUri() { - return mInputUri; - } - - public Uri getOutputUri() { - return mOutputUri; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeParcelable(mInputUri, 0); - dest.writeParcelable(mOutputUri, 0); - } - - public static final Creator CREATOR = new Creator() { - public MimeParsingParcel createFromParcel(final Parcel source) { - return new MimeParsingParcel(source); - } - - public MimeParsingParcel[] newArray(final int size) { - return new MimeParsingParcel[size]; - } - }; - -} - diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java index 3dda47ac5..ddaf40010 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java @@ -58,13 +58,10 @@ import org.openintents.openpgp.OpenPgpSignatureResult; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; -import org.sufficientlysecure.keychain.operations.results.MimeParsingResult; 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.MimeParsingParcel; -import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.ui.base.QueueingCryptoOperationFragment; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.StatusHolder; import org.sufficientlysecure.keychain.ui.DecryptListFragment.DecryptFilesAdapter.ViewModel; diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/InputDataOperationTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/InputDataOperationTest.java new file mode 100644 index 000000000..71673bdb7 --- /dev/null +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/InputDataOperationTest.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2014 Vincent Breitmoser + * + * 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 . + */ + +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.ShadowContentProvider; +import org.robolectric.shadows.ShadowContentResolver; +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.atLeast; +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); + + PgpDecryptVerifyInputParcel decryptInput = new PgpDecryptVerifyInputParcel(); + InputDataParcel input = new InputDataParcel(fakeInputUri, decryptInput); + + 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 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())); + + + } + +} -- cgit v1.2.3