package com.fsck.k9.provider; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; 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.os.ParcelFileDescriptor; import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.FileProvider; import android.text.TextUtils; import timber.log.Timber; import com.fsck.k9.BuildConfig; import com.fsck.k9.K9; import com.fsck.k9.mailstore.util.FileFactory; import org.apache.james.mime4j.codec.Base64InputStream; import org.apache.james.mime4j.codec.QuotedPrintableInputStream; import org.apache.james.mime4j.util.MimeUtil; import org.openintents.openpgp.util.ParcelFileDescriptorUtil; public class DecryptedFileProvider extends FileProvider { private static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".decryptedfileprovider"; private static final String DECRYPTED_CACHE_DIRECTORY = "decrypted"; private static final long FILE_DELETE_THRESHOLD_MILLISECONDS = 3 * 60 * 1000; private static final Object cleanupReceiverMonitor = new Object(); private static DecryptedFileProviderCleanupReceiver cleanupReceiver = null; public static FileFactory getFileFactory(Context context) { final Context applicationContext = context.getApplicationContext(); return new FileFactory() { @Override public File createFile() throws IOException { registerFileCleanupReceiver(applicationContext); File decryptedTempDirectory = getDecryptedTempDirectory(applicationContext); return File.createTempFile("decrypted-", null, decryptedTempDirectory); } }; } @Nullable public static Uri getUriForProvidedFile(@NonNull Context context, File file, @Nullable String encoding, @Nullable String mimeType) throws IOException { try { Uri.Builder uriBuilder = FileProvider.getUriForFile(context, AUTHORITY, file).buildUpon(); if (mimeType != null) { uriBuilder.appendQueryParameter("mime_type", mimeType); } if (encoding != null) { uriBuilder.appendQueryParameter("encoding", encoding); } return uriBuilder.build(); } catch (IllegalArgumentException e) { return null; } } public static boolean deleteOldTemporaryFiles(Context context) { File tempDirectory = getDecryptedTempDirectory(context); boolean allFilesDeleted = true; long deletionThreshold = System.currentTimeMillis() - FILE_DELETE_THRESHOLD_MILLISECONDS; for (File tempFile : tempDirectory.listFiles()) { long lastModified = tempFile.lastModified(); if (lastModified < deletionThreshold) { boolean fileDeleted = tempFile.delete(); if (!fileDeleted) { Timber.e("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.isDebug()) { String timeLeftStr = String.format( Locale.ENGLISH, "%.2f", (lastModified - deletionThreshold) / 1000 / 60.0); Timber.e("Not deleting temp file (for another %s minutes)", timeLeftStr); } allFilesDeleted = false; } } return allFilesDeleted; } private static File getDecryptedTempDirectory(Context context) { File directory = new File(context.getCacheDir(), DECRYPTED_CACHE_DIRECTORY); if (!directory.exists()) { if (!directory.mkdir()) { Timber.e("Error creating directory: %s", 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 ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { ParcelFileDescriptor pfd = super.openFile(uri, "r"); InputStream decodedInputStream; String encoding = uri.getQueryParameter("encoding"); if (MimeUtil.isBase64Encoding(encoding)) { InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(pfd); decodedInputStream = new Base64InputStream(inputStream); } else if (MimeUtil.isQuotedPrintableEncoded(encoding)) { InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(pfd); decodedInputStream = new QuotedPrintableInputStream(inputStream); } else { // no or unknown encoding if (!TextUtils.isEmpty(encoding)) { Timber.e("unsupported encoding, returning raw stream"); } return pfd; } try { return ParcelFileDescriptorUtil.pipeFrom(decodedInputStream); } catch (IOException e) { // not strictly a FileNotFoundException, but failure to create a pipe is basically "can't access right now" throw new FileNotFoundException(); } } @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; } Timber.d("Unregistering temp file cleanup receiver"); context.unregisterReceiver(cleanupReceiver); cleanupReceiver = null; } } private static void registerFileCleanupReceiver(Context context) { synchronized (cleanupReceiverMonitor) { if (cleanupReceiver != null) { return; } Timber.d("Registering temp file cleanup receiver"); cleanupReceiver = new DecryptedFileProviderCleanupReceiver(); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(Intent.ACTION_SCREEN_OFF); context.registerReceiver(cleanupReceiver, intentFilter); } } private static class DecryptedFileProviderCleanupReceiver 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!"); } Timber.d("Cleaning up temp files"); boolean allFilesDeleted = deleteOldTemporaryFiles(context); if (allFilesDeleted) { unregisterFileCleanupReceiver(context); } } } }