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

Commit be453804 authored by Vincent Breitmoser's avatar Vincent Breitmoser
Browse files

use AttachmentTempFileProvider to expose file-backed content uris

parent 0617d425
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -422,7 +422,17 @@
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/decrypted_file_provider_paths" />
        </provider>

        <provider
            android:name=".provider.AttachmentTempFileProvider"
            android:authorities="${applicationId}.tempfileprovider"
            android:exported="false"
            android:grantUriPermissions="true">

            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/temp_file_provider_paths" />
        </provider>

    </application>
+2 −2
Original line number Diff line number Diff line
@@ -127,13 +127,13 @@ public class AttachmentPresenter {
    }

    public void addAttachment(AttachmentViewInfo attachmentViewInfo) {
        if (attachments.containsKey(attachmentViewInfo.uri)) {
        if (attachments.containsKey(attachmentViewInfo.internalUri)) {
            throw new IllegalStateException("Received the same attachmentViewInfo twice!");
        }

        int loaderId = getNextFreeLoaderId();
        Attachment attachment = Attachment.createAttachment(
                attachmentViewInfo.uri, loaderId, attachmentViewInfo.mimeType);
                attachmentViewInfo.internalUri, loaderId, attachmentViewInfo.mimeType);
        attachment = attachment.deriveWithMetadataLoaded(
                attachmentViewInfo.mimeType, attachmentViewInfo.displayName, attachmentViewInfo.size);

+1 −1
Original line number Diff line number Diff line
@@ -69,7 +69,7 @@ public class AttachmentResolver {
                    String contentId = part.getContentId();
                    if (contentId != null) {
                        AttachmentViewInfo attachmentInfo = attachmentInfoExtractor.extractAttachmentInfo(part);
                        result.put(contentId, attachmentInfo.uri);
                        result.put(contentId, attachmentInfo.internalUri);
                    }
                } catch (MessagingException e) {
                    Log.e(K9.LOG_TAG, "Error extracting attachment info", e);
+3 −5
Original line number Diff line number Diff line
@@ -17,20 +17,18 @@ public class AttachmentViewInfo {
     * A content provider URI that can be used to retrieve the decoded attachment.
     * <p/>
     * Note: All content providers must support an alternative MIME type appended as last URI segment.
     *
     * @see com.fsck.k9.ui.messageview.AttachmentController#getAttachmentUriForMimeType(AttachmentViewInfo, String)
     */
    public final Uri uri;
    public final Uri internalUri;
    public final boolean inlineAttachment;
    public final Part part;
    public final boolean isContentAvailable;

    public AttachmentViewInfo(String mimeType, String displayName, long size, Uri uri, boolean inlineAttachment,
    public AttachmentViewInfo(String mimeType, String displayName, long size, Uri internalUri, boolean inlineAttachment,
            Part part, boolean isContentAvailable) {
        this.mimeType = mimeType;
        this.displayName = displayName;
        this.size = size;
        this.uri = uri;
        this.internalUri = internalUri;
        this.inlineAttachment = inlineAttachment;
        this.part = part;
        this.isContentAvailable = isContentAvailable;
+219 −0
Original line number Diff line number Diff line
package com.fsck.k9.provider;


import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.Locale;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.AsyncTask;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.WorkerThread;
import android.support.v4.content.FileProvider;
import android.util.Log;

import com.fsck.k9.BuildConfig;
import com.fsck.k9.K9;
import com.fsck.k9.mail.filter.Hex;
import org.apache.commons.io.IOUtils;


public class AttachmentTempFileProvider extends FileProvider {
    private static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".tempfileprovider";
    private static final String CACHE_DIRECTORY = "temp";
    private static final long FILE_DELETE_THRESHOLD_MILLISECONDS = 3 * 60 * 1000;
    private static final Object tempFileWriteMonitor = new Object();
    private static final Object cleanupReceiverMonitor = new Object();


    private static AttachmentTempFileProviderCleanupReceiver cleanupReceiver = null;


    @WorkerThread
    public static Uri createTempUriForContentUri(Context context, Uri uri) throws IOException {
        Context applicationContext = context.getApplicationContext();

        File tempFile = getTempFileForUri(uri, applicationContext);
        writeUriContentToTempFileIfNotExists(context, uri, tempFile);
        Uri tempFileUri = FileProvider.getUriForFile(context, AUTHORITY, tempFile);

        registerFileCleanupReceiver(applicationContext);

        return tempFileUri;
    }

    @NonNull
    private static File getTempFileForUri(Uri uri, Context context) {
        Context applicationContext = context.getApplicationContext();

        String tempFilename = getTempFilenameForUri(uri);
        File tempDirectory = getTempFileDirectory(applicationContext);
        return new File(tempDirectory, tempFilename);
    }

    private static String getTempFilenameForUri(Uri uri) {
        try {
            byte[] digest = MessageDigest.getInstance("SHA-1").digest(uri.toString().getBytes());
            return new String(Hex.encodeHex(digest));
        } catch (NoSuchAlgorithmException e) {
            throw new AssertionError(e);
        }
    }

    private static void writeUriContentToTempFileIfNotExists(Context context, Uri uri, File tempFile)
            throws IOException {
        synchronized (tempFileWriteMonitor) {
            if (tempFile.exists()) {
                return;
            }

            FileOutputStream outputStream = new FileOutputStream(tempFile);
            InputStream inputStream = context.getContentResolver().openInputStream(uri);
            if (inputStream == null) {
                throw new IOException("Failed to resolve content at uri: " + uri);
            }
            IOUtils.copy(inputStream, outputStream);

            outputStream.close();
            IOUtils.closeQuietly(inputStream);
        }
    }

    public static Uri getMimeTypeUri(Uri contentUri, String mimeType) {
        if (!AUTHORITY.equals(contentUri.getAuthority())) {
            throw new IllegalArgumentException("Can only call this method for URIs within this authority!");
        }
        if (contentUri.getQueryParameter("mime_type") != null) {
            throw new IllegalArgumentException("Can only call this method for not yet typed URIs!");
        }
        return contentUri.buildUpon().appendQueryParameter("mime_type", mimeType).build();
    }

    public static boolean deleteOldTemporaryFiles(Context context) {
        File tempDirectory = getTempFileDirectory(context);
        boolean allFilesDeleted = true;
        long deletionThreshold = new Date().getTime() - FILE_DELETE_THRESHOLD_MILLISECONDS;
        for (File tempFile : tempDirectory.listFiles()) {
            long lastModified = tempFile.lastModified();
            if (lastModified < deletionThreshold) {
                boolean fileDeleted = tempFile.delete();
                if (!fileDeleted) {
                    Log.e(K9.LOG_TAG, "Failed to delete temporary file");
                    // TODO really do this? might cause our service to stay up indefinitely if a file can't be deleted
                    allFilesDeleted = false;
                }
            } else {
                if (K9.DEBUG) {
                    String timeLeftStr = String.format(
                            Locale.ENGLISH, "%.2f", (lastModified - deletionThreshold) / 1000 / 60.0);
                    Log.e(K9.LOG_TAG, "Not deleting temp file (for another " + timeLeftStr + " minutes)");
                }
                allFilesDeleted = false;
            }
        }

        return allFilesDeleted;
    }

    private static File getTempFileDirectory(Context context) {
        File directory = new File(context.getCacheDir(), CACHE_DIRECTORY);
        if (!directory.exists()) {
            if (!directory.mkdir()) {
                Log.e(K9.LOG_TAG, "Error creating directory: " + directory.getAbsolutePath());
            }
        }

        return directory;
    }


    @Override
    public String getType(Uri uri) {
        return uri.getQueryParameter("mime_type");
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void onTrimMemory(int level) {
        if (level < TRIM_MEMORY_COMPLETE) {
            return;
        }
        final Context context = getContext();
        if (context == null) {
            return;
        }

        new AsyncTask<Void,Void,Void>() {
            @Override
            protected Void doInBackground(Void... voids) {
                deleteOldTemporaryFiles(context);
                return null;
            }
        }.execute();

        unregisterFileCleanupReceiver(context);
    }

    private static void unregisterFileCleanupReceiver(Context context) {
        synchronized (cleanupReceiverMonitor) {
            if (cleanupReceiver == null) {
                return;
            }

            if (K9.DEBUG) {
                Log.d(K9.LOG_TAG, "Unregistering temp file cleanup receiver");
            }
            context.unregisterReceiver(cleanupReceiver);
            cleanupReceiver = null;
        }
    }

    private static void registerFileCleanupReceiver(Context context) {
        synchronized (cleanupReceiverMonitor) {
            if (cleanupReceiver != null) {
                return;
            }
            if (K9.DEBUG) {
                Log.d(K9.LOG_TAG, "Registering temp file cleanup receiver");
            }
            cleanupReceiver = new AttachmentTempFileProviderCleanupReceiver();

            IntentFilter intentFilter = new IntentFilter();
            intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
            context.registerReceiver(cleanupReceiver, intentFilter);
        }
    }

    private static class AttachmentTempFileProviderCleanupReceiver extends BroadcastReceiver {
        @Override
        @MainThread
        public void onReceive(Context context, Intent intent) {
            if (!Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
                throw new IllegalArgumentException("onReceive called with action that isn't screen off!");
            }

            if (K9.DEBUG) {
                Log.d(K9.LOG_TAG, "Cleaning up temp files");
            }

            boolean allFilesDeleted = deleteOldTemporaryFiles(context);
            if (allFilesDeleted) {
                unregisterFileCleanupReceiver(context);
            }
        }
    }
}
Loading