package com.fsck.k9.provider;
import java.io.File;
import java.io.FileOutputStream;
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.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.WorkerThread;
import android.support.v4.content.FileProvider;
import timber.log.Timber;
import com.fsck.k9.BuildConfig;
import com.fsck.k9.K9;
import okio.ByteString;
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) {
return ByteString.encodeUtf8(uri.toString()).sha1().hex();
}
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 = 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 getTempFileDirectory(Context context) {
File directory = new File(context.getCacheDir(), 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 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 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!");
}
Timber.d("Cleaning up temp files");
boolean allFilesDeleted = deleteOldTemporaryFiles(context);
if (allFilesDeleted) {
unregisterFileCleanupReceiver(context);
}
}
}
}