Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 62aa1b87 authored by cketti's avatar cketti
Browse files

Fetch attachments while MessageCompose activity is running

Android allows other apps to access protected content of an app without requesting the
necessary permission when the app returns an Intent with FLAG_GRANT_READ_URI_PERMISSION.
This regularly happens as a result of ACTION_GET_CONTENT, i.e. what we use to pick content
to be attached to a message. Accessing that content only works while the receiving activity
is running. Afterwards accessing the content throws a SecurityException because of the
missing permission.
This commit changes K-9 Mail's behavior to copy the content to a temporary file in K-9's
cache directory while the activity is still running.

Fixes issue 4847, 5821

This also fixes bugs related to the fact that K-9 Mail didn't save a copy of attached content
in the message database.

Fixes issue 1187, 3330, 4930
parent 8d0f697e
Loading
Loading
Loading
Loading
+52 −33
Original line number Diff line number Diff line
@@ -6,31 +6,50 @@
    android:paddingRight="6dip"
    android:paddingTop="6dip"
    android:paddingBottom="6dip">

    <ImageButton
        android:id="@+id/attachment_delete"
        android:src="@drawable/ic_delete"
        android:layout_alignParentRight="true"
        android:layout_height="42dip"
        android:layout_width="42dip" />

    <LinearLayout
        android:layout_width="1dip"
        android:layout_height="42dip"
        android:layout_alignParentLeft="true"
        android:layout_toLeftOf="@id/attachment_delete"
        android:layout_marginLeft="6dip"
        android:layout_marginRight="4dip"
        android:paddingLeft="36dip"
        android:gravity="center_vertical"
        android:background="?attr/messageViewAttachmentBackground"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/attachment_name"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:textAppearance="?android:attr/textAppearanceMedium"
            android:textColor="?android:attr/textColorSecondary"
		android:layout_width="1dip"
		android:layout_height="42dip"
		android:background="?attr/messageViewAttachmentBackground"
		android:paddingLeft="36dip"
            android:singleLine="true"
		android:ellipsize="start"
		android:gravity="center_vertical"
		android:layout_marginLeft="6dip"
		android:layout_marginRight="4dip"
		android:layout_alignParentLeft="true"
		android:layout_toLeftOf="@id/attachment_delete" />
            android:ellipsize="start"/>

        <ProgressBar
            android:id="@+id/progressBar"
            style="?android:attr/progressBarStyleSmall"
            android:layout_width="32dp"
            android:layout_height="fill_parent"
            android:layout_marginLeft="4dp"/>

    </LinearLayout>

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_email_attachment"
        android:layout_marginLeft="1dip"
        android:layout_centerVertical="true" />

</RelativeLayout>
+2 −0
Original line number Diff line number Diff line
@@ -1147,4 +1147,6 @@ Please submit bug reports, contribute new features and ask questions at
    <string name="preposition_for_date">on <xliff:g id="date">%s</xliff:g></string>

    <string name="mark_all_as_read">Mark all as read</string>

    <string name="loading_attachment">Loading attachment…</string>
</resources>
+2 −2
Original line number Diff line number Diff line
@@ -3,12 +3,12 @@ package com.fsck.k9.activity;
import android.os.Bundle;
import android.view.MotionEvent;

import com.actionbarsherlock.app.SherlockActivity;
import com.actionbarsherlock.app.SherlockFragmentActivity;
import com.fsck.k9.activity.K9ActivityCommon.K9ActivityMagic;
import com.fsck.k9.activity.misc.SwipeGestureDetector.OnSwipeGestureListener;


public class K9Activity extends SherlockActivity implements K9ActivityMagic {
public class K9Activity extends SherlockFragmentActivity implements K9ActivityMagic {

    private K9ActivityCommon mBase;

+141 −70
Original line number Diff line number Diff line
@@ -5,19 +5,18 @@ import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.app.Dialog;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcelable;
import android.provider.OpenableColumns;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.text.TextWatcher;
import android.text.util.Rfc822Tokenizer;
import android.util.Log;
@@ -55,6 +54,9 @@ import com.fsck.k9.Identity;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
import com.fsck.k9.activity.loader.AttachmentContentLoader;
import com.fsck.k9.activity.loader.AttachmentInfoLoader;
import com.fsck.k9.activity.misc.Attachment;
import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.controller.MessagingListener;
import com.fsck.k9.crypto.CryptoProvider;
@@ -86,14 +88,13 @@ import org.htmlcleaner.CleanerProperties;
import org.htmlcleaner.HtmlCleaner;
import org.htmlcleaner.SimpleHtmlSerializer;
import org.htmlcleaner.TagNode;
import java.io.File;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -147,6 +148,8 @@ public class MessageCompose extends K9Activity implements OnClickListener {
    private static final String STATE_KEY_QUOTED_TEXT_FORMAT =
            "com.fsck.k9.activity.MessageCompose.quotedTextFormat";

    private static final String LOADER_ARG_ATTACHMENT = "attachment";

    private static final int MSG_PROGRESS_ON = 1;
    private static final int MSG_PROGRESS_OFF = 2;
    private static final int MSG_SKIPPED_ATTACHMENTS = 3;
@@ -218,6 +221,7 @@ public class MessageCompose extends K9Activity implements OnClickListener {
     * have already been added from the restore of the view state.
     */
    private boolean mSourceMessageProcessed = false;
    private int mMaxLoaderId = 0;

    enum Action {
        COMPOSE,
@@ -365,14 +369,6 @@ public class MessageCompose extends K9Activity implements OnClickListener {
    private ContextThemeWrapper mThemeContext;


    static class Attachment implements Serializable {
        private static final long serialVersionUID = 3642382876618963734L;
        public String name;
        public String contentType;
        public long size;
        public Uri uri;
    }

    /**
     * Compose a new message using the given account. If account is null the default account
     * will be used.
@@ -1077,12 +1073,13 @@ public class MessageCompose extends K9Activity implements OnClickListener {
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        ArrayList<Uri> attachments = new ArrayList<Uri>();
        ArrayList<Attachment> attachments = new ArrayList<Attachment>();
        for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
            View view = mAttachments.getChildAt(i);
            Attachment attachment = (Attachment) view.getTag();
            attachments.add(attachment.uri);
            attachments.add(attachment);
        }

        outState.putParcelableArrayList(STATE_KEY_ATTACHMENTS, attachments);
        outState.putBoolean(STATE_KEY_CC_SHOWN, mCcWrapper.getVisibility() == View.VISIBLE);
        outState.putBoolean(STATE_KEY_BCC_SHOWN, mBccWrapper.getVisibility() == View.VISIBLE);
@@ -1104,11 +1101,22 @@ public class MessageCompose extends K9Activity implements OnClickListener {
    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        ArrayList<Parcelable> attachments = savedInstanceState.getParcelableArrayList(STATE_KEY_ATTACHMENTS);

        mAttachments.removeAllViews();
        for (Parcelable p : attachments) {
            Uri uri = (Uri) p;
            addAttachment(uri);
        mMaxLoaderId = 0;

        ArrayList<Attachment> attachments = savedInstanceState.getParcelableArrayList(STATE_KEY_ATTACHMENTS);
        for (Attachment attachment : attachments) {
            addAttachmentView(attachment);
            if (attachment.loaderId > mMaxLoaderId) {
                mMaxLoaderId = attachment.loaderId;
            }

            if (attachment.state == Attachment.LoadingState.URI_ONLY) {
                initAttachmentInfoLoader(attachment);
            } else if (attachment.state == Attachment.LoadingState.METADATA) {
                initAttachmentContentLoader(attachment);
            }
        }

        mReadReceipt = savedInstanceState
@@ -1474,8 +1482,11 @@ public class MessageCompose extends K9Activity implements OnClickListener {
        for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
            Attachment attachment = (Attachment) mAttachments.getChildAt(i).getTag();

            MimeBodyPart bp = new MimeBodyPart(
                new LocalStore.LocalAttachmentBody(attachment.uri, getApplication()));
            if (attachment.state != Attachment.LoadingState.COMPLETE) {
                continue;
            }

            MimeBodyPart bp = new MimeBodyPart(new LocalStore.TempFileBody(attachment.filename));

            /*
             * Correctly encode the filename here. Otherwise the whole
@@ -1911,70 +1922,130 @@ public class MessageCompose extends K9Activity implements OnClickListener {
    }

    private void addAttachment(Uri uri, String contentType) {
        long size = -1;
        String name = null;
        Attachment attachment = new Attachment();
        attachment.state = Attachment.LoadingState.URI_ONLY;
        attachment.uri = uri;
        attachment.contentType = contentType;
        attachment.loaderId = ++mMaxLoaderId;

        ContentResolver contentResolver = getContentResolver();
        addAttachmentView(attachment);

        Cursor metadataCursor = contentResolver.query(
                                    uri,
                                    new String[] { OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE },
                                    null,
                                    null,
                                    null);
        initAttachmentInfoLoader(attachment);
    }

        if (metadataCursor != null) {
            try {
                if (metadataCursor.moveToFirst()) {
                    name = metadataCursor.getString(0);
                    size = metadataCursor.getInt(1);
    private void initAttachmentInfoLoader(Attachment attachment) {
        LoaderManager loaderManager = getSupportLoaderManager();
        Bundle bundle = new Bundle();
        bundle.putParcelable(LOADER_ARG_ATTACHMENT, attachment);
        loaderManager.initLoader(attachment.loaderId, bundle, mAttachmentInfoLoaderCallback);
    }
            } finally {
                metadataCursor.close();

    private void initAttachmentContentLoader(Attachment attachment) {
        LoaderManager loaderManager = getSupportLoaderManager();
        Bundle bundle = new Bundle();
        bundle.putParcelable(LOADER_ARG_ATTACHMENT, attachment);
        loaderManager.initLoader(attachment.loaderId, bundle, mAttachmentContentLoaderCallback);
    }

    private void addAttachmentView(Attachment attachment) {
        boolean hasMetadata = (attachment.state != Attachment.LoadingState.URI_ONLY);
        boolean isLoadingComplete = (attachment.state == Attachment.LoadingState.COMPLETE);

        View view = getLayoutInflater().inflate(R.layout.message_compose_attachment, mAttachments, false);
        TextView nameView = (TextView) view.findViewById(R.id.attachment_name);
        View progressBar = view.findViewById(R.id.progressBar);

        if (hasMetadata) {
            nameView.setText(attachment.name);
        } else {
            nameView.setText(R.string.loading_attachment);
        }

        if (name == null) {
            name = uri.getLastPathSegment();
        progressBar.setVisibility(isLoadingComplete ? View.GONE : View.VISIBLE);

        ImageButton delete = (ImageButton) view.findViewById(R.id.attachment_delete);
        delete.setOnClickListener(MessageCompose.this);
        delete.setTag(view);

        view.setTag(attachment);
        mAttachments.addView(view);
    }

        String usableContentType = contentType;
        if ((usableContentType == null) || (usableContentType.indexOf('*') != -1)) {
            usableContentType = contentResolver.getType(uri);
    private View getAttachmentView(int loaderId) {
        for (int i = 0, childCount = mAttachments.getChildCount(); i < childCount; i++) {
            View view = mAttachments.getChildAt(i);
            Attachment tag = (Attachment) view.getTag();
            if (tag != null && tag.loaderId == loaderId) {
                return view;
            }
        if (usableContentType == null) {
            usableContentType = MimeUtility.getMimeTypeByExtension(name);
        }

        if (size <= 0) {
            String uriString = uri.toString();
            if (uriString.startsWith("file://")) {
                Log.v(K9.LOG_TAG, uriString.substring("file://".length()));
                File f = new File(uriString.substring("file://".length()));
                size = f.length();
            } else {
                Log.v(K9.LOG_TAG, "Not a file: " + uriString);
        return null;
    }
        } else {
            Log.v(K9.LOG_TAG, "old attachment.size: " + size);

    private LoaderManager.LoaderCallbacks<Attachment> mAttachmentInfoLoaderCallback =
            new LoaderManager.LoaderCallbacks<Attachment>() {
        @Override
        public Loader<Attachment> onCreateLoader(int id, Bundle args) {
            Attachment attachment = args.getParcelable(LOADER_ARG_ATTACHMENT);
            return new AttachmentInfoLoader(MessageCompose.this, attachment);
        }
        Log.v(K9.LOG_TAG, "new attachment.size: " + size);

        Attachment attachment = new Attachment();
        attachment.uri = uri;
        attachment.contentType = usableContentType;
        attachment.name = name;
        attachment.size = size;
        @Override
        public void onLoadFinished(Loader<Attachment> loader, Attachment attachment) {
            int loaderId = loader.getId();

            View view = getAttachmentView(loaderId);
            if (view != null) {
                view.setTag(attachment);

        View view = getLayoutInflater().inflate(R.layout.message_compose_attachment, mAttachments, false);
                TextView nameView = (TextView) view.findViewById(R.id.attachment_name);
        ImageButton delete = (ImageButton)view.findViewById(R.id.attachment_delete);
                nameView.setText(attachment.name);
        delete.setOnClickListener(this);
        delete.setTag(view);

                attachment.loaderId = ++mMaxLoaderId;
                initAttachmentContentLoader(attachment);
            }

            getSupportLoaderManager().destroyLoader(loaderId);
        }

        @Override
        public void onLoaderReset(Loader<Attachment> loader) {
        }
    };

    private LoaderManager.LoaderCallbacks<Attachment> mAttachmentContentLoaderCallback =
            new LoaderManager.LoaderCallbacks<Attachment>() {
        @Override
        public Loader<Attachment> onCreateLoader(int id, Bundle args) {
            Attachment attachment = args.getParcelable(LOADER_ARG_ATTACHMENT);
            return new AttachmentContentLoader(MessageCompose.this, attachment);
        }

        @Override
        public void onLoadFinished(Loader<Attachment> loader, Attachment attachment) {
            int loaderId = loader.getId();

            View view = getAttachmentView(loaderId);
            if (view != null) {
                if (attachment.state == Attachment.LoadingState.COMPLETE) {
                    view.setTag(attachment);
        mAttachments.addView(view);

                    View progressBar = view.findViewById(R.id.progressBar);
                    progressBar.setVisibility(View.GONE);
                } else {
                    mAttachments.removeView(view);
                }
            }

            getSupportLoaderManager().destroyLoader(loaderId);
        }

        @Override
        public void onLoaderReset(Loader<Attachment> loader) {
        }
    };


    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+81 −0
Original line number Diff line number Diff line
package com.fsck.k9.activity.loader;

import android.content.Context;
import android.support.v4.content.AsyncTaskLoader;
import android.util.Log;

import com.fsck.k9.K9;
import com.fsck.k9.activity.misc.Attachment;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * Loader to fetch the content of an attachment.
 *
 * This will copy the data to a temporary file in our app's cache directory.
 */
public class AttachmentContentLoader extends AsyncTaskLoader<Attachment> {
    private static final String FILENAME_PREFIX = "attachment";

    private final Attachment mAttachment;

    public AttachmentContentLoader(Context context, Attachment attachment) {
        super(context);
        mAttachment = attachment;
    }

    @Override
    protected void onStartLoading() {
        if (mAttachment.state == Attachment.LoadingState.COMPLETE) {
            deliverResult(mAttachment);
        }

        if (takeContentChanged() || mAttachment.state == Attachment.LoadingState.METADATA) {
            forceLoad();
        }
    }

    @Override
    public Attachment loadInBackground() {
        Context context = getContext();

        try {
            File file = File.createTempFile(FILENAME_PREFIX, null, context.getCacheDir());
            file.deleteOnExit();

            if (K9.DEBUG) {
                Log.v(K9.LOG_TAG, "Saving attachment to " + file.getAbsolutePath());
            }

            InputStream in = context.getContentResolver().openInputStream(mAttachment.uri);
            try {
                FileOutputStream out = new FileOutputStream(file);
                try {
                    IOUtils.copy(in, out);
                } finally {
                    out.close();
                }
            } finally {
                in.close();
            }

            mAttachment.filename = file.getAbsolutePath();
            mAttachment.state = Attachment.LoadingState.COMPLETE;

            return mAttachment;
        } catch (IOException e) {
            e.printStackTrace();
        }

        mAttachment.filename = null;
        mAttachment.state = Attachment.LoadingState.CANCELLED;

        return mAttachment;
    }
}
Loading