[go: nahoru, domu]

blob: 319f162a4f702b7bb1507be89f5ed99c29812371 [file] [log] [blame]
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.browser.browseractions;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import android.content.ClipData;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.UiThread;
import androidx.concurrent.futures.ResolvableFuture;
import androidx.core.content.FileProvider;
import androidx.core.util.AtomicFile;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* The class to pass images asynchronously between different Browser Services provider and Browser
* client.
*
* Call {@link #saveBitmap} to save the image and {@link #loadBitmap} to read it.
*
* @deprecated Browser Actions are deprecated as of release 1.2.0.
* @hide
*/
@Deprecated
@RestrictTo(LIBRARY)
public final class BrowserServiceFileProvider extends FileProvider {
private static final String TAG = "BrowserServiceFP";
private static final String AUTHORITY_SUFFIX = ".image_provider";
private static final String CONTENT_SCHEME = "content";
private static final String FILE_SUB_DIR = "image_provider";
private static final String FILE_SUB_DIR_NAME = "image_provider_images/";
private static final String FILE_EXTENSION = ".png";
private static final String CLIP_DATA_LABEL = "image_provider_uris";
private static final String LAST_CLEANUP_TIME_KEY = "last_cleanup_time";
@SuppressWarnings("WeakerAccess") /* synthetic access */
static Object sFileCleanupLock = new Object();
private static class FileCleanupTask extends AsyncTask<Void, Void, Void> {
private final Context mAppContext;
private static final long IMAGE_RETENTION_DURATION = TimeUnit.DAYS.toMillis(7);
private static final long CLEANUP_REQUIRED_TIME_SPAN = TimeUnit.DAYS.toMillis(7);
private static final long DELETION_FAILED_REATTEMPT_DURATION = TimeUnit.DAYS.toMillis(1);
FileCleanupTask(Context context) {
super();
mAppContext = context.getApplicationContext();
}
@Override
protected Void doInBackground(Void... params) {
SharedPreferences prefs = mAppContext.getSharedPreferences(
mAppContext.getPackageName() + AUTHORITY_SUFFIX, Context.MODE_PRIVATE);
if (!shouldCleanUp(prefs)) return null;
synchronized (sFileCleanupLock) {
boolean allFilesDeletedSuccessfully = true;
File path = new File(mAppContext.getFilesDir(), FILE_SUB_DIR);
if (!path.exists()) return null;
File[] files = path.listFiles();
long retentionDate = System.currentTimeMillis() - IMAGE_RETENTION_DURATION;
for (File file : files) {
if (!isImageFile(file)) continue;
long lastModified = file.lastModified();
if (lastModified < retentionDate && !file.delete()) {
Log.e(TAG, "Fail to delete image: " + file.getAbsoluteFile());
allFilesDeletedSuccessfully = false;
}
}
// If fail to delete some files, kill off clean up task after one day.
long lastCleanUpTime;
if (allFilesDeletedSuccessfully) {
lastCleanUpTime = System.currentTimeMillis();
} else {
lastCleanUpTime = System.currentTimeMillis() - CLEANUP_REQUIRED_TIME_SPAN
+ DELETION_FAILED_REATTEMPT_DURATION;
}
Editor editor = prefs.edit();
editor.putLong(LAST_CLEANUP_TIME_KEY, lastCleanUpTime);
editor.apply();
}
return null;
}
private static boolean isImageFile(File file) {
String filename = file.getName();
return filename.endsWith("." + FILE_EXTENSION);
}
private static boolean shouldCleanUp(SharedPreferences prefs) {
long lastCleanup = prefs.getLong(LAST_CLEANUP_TIME_KEY, System.currentTimeMillis());
return System.currentTimeMillis() > lastCleanup + CLEANUP_REQUIRED_TIME_SPAN;
}
}
private static class FileSaveTask extends AsyncTask<String, Void, Void> {
private final Context mAppContext;
private final String mFilename;
private final Bitmap mBitmap;
private final Uri mFileUri;
private final ResolvableFuture<Uri> mResultFuture;
FileSaveTask(Context context, String filename, Bitmap bitmap, Uri fileUri,
ResolvableFuture<Uri> resultFuture) {
super();
mAppContext = context.getApplicationContext();
mFilename = filename;
mBitmap = bitmap;
mFileUri = fileUri;
mResultFuture = resultFuture;
}
@Override
protected Void doInBackground(String... params) {
saveFileIfNeededBlocking();
return null;
}
@Override
protected void onPostExecute(Void result) {
new FileCleanupTask(mAppContext).executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
}
private void saveFileIfNeededBlocking() {
File path = new File(mAppContext.getFilesDir(), FILE_SUB_DIR);
synchronized (sFileCleanupLock) {
if (!path.exists() && !path.mkdir()) {
mResultFuture.setException(new IOException("Could not create file directory."));
return;
}
File img = new File(path, mFilename + FILE_EXTENSION);
if (img.exists()) {
mResultFuture.set(mFileUri);
} else {
saveFileBlocking(img);
}
img.setLastModified(System.currentTimeMillis());
}
}
private void saveFileBlocking(File img) {
FileOutputStream fOut = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
AtomicFile atomicFile = new AtomicFile(img);
try {
fOut = atomicFile.startWrite();
mBitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut);
fOut.close();
atomicFile.finishWrite(fOut);
mResultFuture.set(mFileUri);
} catch (IOException e) {
atomicFile.failWrite(fOut);
mResultFuture.setException(e);
}
} else {
try {
fOut = new FileOutputStream(img);
mBitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut);
fOut.close();
mResultFuture.set(mFileUri);
} catch (IOException e) {
mResultFuture.setException(e);
}
}
}
}
/**
* Request a {@link Uri} used to access the bitmap through the file provider.
* @param context The {@link Context} used to generate the uri, save the bitmap and grant the
* read permission.
* @param bitmap The {@link Bitmap} to be saved and access through the file provider.
* @param name The name of the bitmap.
* @param version The version number of the bitmap. Note: This plus the name decides the
* filename of the bitmap. If it matches with existing file, bitmap will skip
* saving.
* @return A {@link ResolvableFuture} that will be fulfilled with the uri of the bitmap once
* file writing has completed or an IOException describing the reason for failure.
*/
@UiThread
@NonNull
public static ResolvableFuture<Uri> saveBitmap(@NonNull Context context, @NonNull Bitmap bitmap,
@NonNull String name, int version) {
String filename = name + "_" + Integer.toString(version);
Uri uri = generateUri(context, filename);
ResolvableFuture<Uri> result = ResolvableFuture.create();
new FileSaveTask(context, filename, bitmap, uri, result)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
return result;
}
private static Uri generateUri(Context context, String filename) {
String fileName = FILE_SUB_DIR_NAME + filename + FILE_EXTENSION;
return new Uri.Builder()
.scheme(CONTENT_SCHEME)
.authority(context.getPackageName() + AUTHORITY_SUFFIX)
.path(fileName)
.build();
}
/**
* Grant the read permission to a list of {@link Uri} sent through a {@link Intent}.
* @param intent The sending Intent which holds a list of Uri.
* @param uris A list of Uri generated by saveBitmap(Context, Bitmap, String, int,
* List<String>), if null, nothing will be done.
* @param context The context requests to grant the permission.
*/
public static void grantReadPermission(@NonNull Intent intent, @Nullable List<Uri> uris,
@NonNull Context context) {
if (uris == null || uris.size() == 0) return;
ContentResolver resolver = context.getContentResolver();
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
ClipData clipData = ClipData.newUri(resolver, CLIP_DATA_LABEL, uris.get(0));
for (int i = 1; i < uris.size(); i++) {
clipData.addItem(new ClipData.Item(uris.get(i)));
}
intent.setClipData(clipData);
}
/**
* Asynchronously loads a {@link Bitmap} from the uri generated by {@link #saveBitmap}.
* @param resolver {@link ContentResolver} to access the Bitmap.
* @param uri {@link Uri} pointing to the Bitmap.
* @return A {@link ListenableFuture} that will be fulfilled with the Bitmap once the load has
* completed or with an IOException describing the reason for failure.
*/
@NonNull
public static ListenableFuture<Bitmap> loadBitmap(@NonNull final ContentResolver resolver,
@NonNull final Uri uri) {
final ResolvableFuture<Bitmap> result = ResolvableFuture.create();
AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
@Override
public void run() {
try {
ParcelFileDescriptor descriptor = resolver.openFileDescriptor(uri, "r");
if (descriptor == null) {
result.setException(new FileNotFoundException());
return;
}
FileDescriptor fileDescriptor = descriptor.getFileDescriptor();
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
descriptor.close();
if (bitmap == null) {
result.setException(new IOException("File could not be decoded."));
return;
}
result.set(bitmap);
} catch (IOException e) {
result.setException(e);
}
}
});
return result;
}
}