Commit a1a06191 authored by Gayane Petrosyan's avatar Gayane Petrosyan Committed by Commit Bot

[QRCode Android] Save bitmaps to external storage.

This CL only saves the bitmap. Adding to complete downloads will be
follow up.

Bug: 993920

Change-Id: I917154b18f456cadcdf29d8f62474f6e4593d941
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2062877
Commit-Queue: Gayane Petrosyan <gayane@chromium.org>
Reviewed-by: default avatarMin Qin <qinmin@chromium.org>
Reviewed-by: default avatarKristi Park <kristipark@chromium.org>
Cr-Commit-Position: refs/heads/master@{#744399}
parent 599c0ac8
...@@ -5,10 +5,13 @@ ...@@ -5,10 +5,13 @@
package org.chromium.chrome.browser.share; package org.chromium.chrome.browser.share;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import android.os.Environment;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.ApplicationState; import org.chromium.base.ApplicationState;
import org.chromium.base.ApplicationStatus; import org.chromium.base.ApplicationStatus;
...@@ -26,6 +29,7 @@ import org.chromium.ui.base.Clipboard; ...@@ -26,6 +29,7 @@ import org.chromium.ui.base.Clipboard;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Locale;
/** /**
* Utility class for file operations for image data. * Utility class for file operations for image data.
...@@ -37,9 +41,11 @@ public class ShareImageFileUtils { ...@@ -37,9 +41,11 @@ public class ShareImageFileUtils {
* Directory name for shared images. * Directory name for shared images.
* *
* Named "screenshot" for historical reasons as we only initially shared screenshot images. * Named "screenshot" for historical reasons as we only initially shared screenshot images.
* TODO(crbug.com/1055886): consider changing the directory name.
*/ */
private static final String SHARE_IMAGES_DIRECTORY_NAME = "screenshot"; private static final String SHARE_IMAGES_DIRECTORY_NAME = "screenshot";
private static final String JPEG_EXTENSION = ".jpg"; private static final String JPEG_EXTENSION = ".jpg";
private static final String FILE_NUMBER_FORMAT = " (%d)";
/** /**
* Delete the |file|, if the |file| is a directory, delete the files and directories in the * Delete the |file|, if the |file| is a directory, delete the files and directories in the
...@@ -130,6 +136,7 @@ public class ShareImageFileUtils { ...@@ -130,6 +136,7 @@ public class ShareImageFileUtils {
/** /**
* Temporarily saves the given set of JPEG bytes and provides that URI to a callback for * Temporarily saves the given set of JPEG bytes and provides that URI to a callback for
* sharing. * sharing.
*
* @param context The context used to trigger the share action. * @param context The context used to trigger the share action.
* @param jpegImageData The image data to be shared in jpeg format. * @param jpegImageData The image data to be shared in jpeg format.
* @param callback A provided callback function which will act on the generated URI. * @param callback A provided callback function which will act on the generated URI.
...@@ -140,47 +147,176 @@ public class ShareImageFileUtils { ...@@ -140,47 +147,176 @@ public class ShareImageFileUtils {
Log.w(TAG, "Share failed -- Received image contains no data."); Log.w(TAG, "Share failed -- Received image contains no data.");
return; return;
} }
OnImageSaveListener listener = new OnImageSaveListener() {
@Override
public void onImageSaved(File imageFile) {
callback.onResult(ContentUriUtils.getContentUriFromFile(imageFile));
}
@Override
public void onImageSaveError() {}
};
String fileName = String.valueOf(System.currentTimeMillis());
saveImage(fileName, "", listener, (fos) -> { writeImageData(fos, jpegImageData); }, true);
}
/**
* Saves bitmap to external storage directory.
*
* @param context The Context to use for determining download location.
* @param filename The filename without extension.
* @param bitmap The Bitmap to download.
* @param listener The OnImageSaveListener to notify the download results.
*/
public static void saveBitmapToExternalStorage(
final Context context, String fileName, Bitmap bitmap, OnImageSaveListener listener) {
String filePath = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).getPath();
saveImage(fileName, filePath, listener, (fos) -> { writeBitmap(fos, bitmap); }, false);
}
/**
* Interface for notifying bitmap download result.
*/
public interface OnImageSaveListener {
void onImageSaved(File imageFile);
void onImageSaveError();
}
new AsyncTask<Uri>() { /**
* Interface for writing image information to a output stream.
*/
private interface FileOutputStreamWriter {
void write(FileOutputStream fos) throws IOException;
}
/**
* Saves image to the given file.
*
* @param fileName The File instance of a destination file.
* @param filePath The File instance of a destination file.
* @param listener The OnImageSaveListener to notify the download results.
* @param writer The FileOutputStreamWriter that writes to given stream.
* @param isTemporary Indicates whether image should be save to a temporary file.
*/
private static void saveImage(String fileName, String filePath, OnImageSaveListener listener,
FileOutputStreamWriter writer, boolean isTemporary) {
new AsyncTask<File>() {
@Override @Override
protected Uri doInBackground() { protected File doInBackground() {
FileOutputStream fOut = null; FileOutputStream fOut = null;
File destFile = null;
try { try {
File path = new File(UiUtils.getDirectoryForImageCapture(context), destFile = createFile(fileName, filePath, isTemporary);
SHARE_IMAGES_DIRECTORY_NAME); if (destFile != null && destFile.exists()) {
if (path.exists() || path.mkdir()) { fOut = new FileOutputStream(destFile);
File saveFile = File.createTempFile( writer.write(fOut);
String.valueOf(System.currentTimeMillis()), JPEG_EXTENSION, path);
fOut = new FileOutputStream(saveFile);
fOut.write(jpegImageData);
fOut.flush();
return ContentUriUtils.getContentUriFromFile(saveFile);
} else { } else {
Log.w(TAG, "Share failed -- Unable to create share image directory."); Log.w(TAG,
"Share failed -- Unable to create or write to destination file.");
} }
} catch (IOException ie) { } catch (IOException ie) {
// Ignore exception. cancel(true);
} finally { } finally {
StreamUtil.closeQuietly(fOut); StreamUtil.closeQuietly(fOut);
} }
return null; return destFile;
}
@Override
protected void onCancelled() {
listener.onImageSaveError();
} }
@Override @Override
protected void onPostExecute(Uri imageUri) { protected void onPostExecute(File imageFile) {
if (imageUri == null) { if (imageFile == null) {
listener.onImageSaveError();
return; return;
} }
if (ApplicationStatus.getStateForApplication() if (ApplicationStatus.getStateForApplication()
== ApplicationState.HAS_DESTROYED_ACTIVITIES) { == ApplicationState.HAS_DESTROYED_ACTIVITIES) {
return; return;
} }
callback.onResult(imageUri); listener.onImageSaved(imageFile);
} }
}.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
/**
* Creates file with specified path, name and extension.
*
* @param filePath The file path a destination file.
* @param fileName The file name a destination file.
* @param isTemporary Indicates whether image should be save to a temporary file.
*
* @return The new File object.
*/
private static File createFile(String fileName, String filePath, boolean isTemporary)
throws IOException {
File path;
if (filePath.isEmpty()) {
path = getSharedFilesDirectory();
} else {
path = new File(filePath);
}
File newFile = null;
if (path.exists() || path.mkdir()) {
if (isTemporary) {
newFile = File.createTempFile(fileName, JPEG_EXTENSION, path);
} else {
newFile = getNextAvailableFile(filePath, fileName, JPEG_EXTENSION);
}
}
return newFile;
}
/**
* Returns next available file for the given fileName.
*
* @param filePath The file path a destination file.
* @param fileName The file name a destination file.
* @param extension The extension a destination file.
*
* @return The new File object.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public static File getNextAvailableFile(String filePath, String fileName, String extension)
throws IOException {
File destFile = new File(filePath, fileName + extension);
int num = 0;
while (destFile.exists()) {
destFile = new File(filePath,
fileName + String.format(Locale.getDefault(), FILE_NUMBER_FORMAT, ++num)
+ extension);
}
destFile.createNewFile();
return destFile;
}
/**
* Writes given bitmap to into the given fos.
*
* @param fos The FileOutputStream to write to.
* @param bitmap The Bitmap to write.
*/
private static void writeBitmap(FileOutputStream fos, Bitmap bitmap) throws IOException {
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
}
/**
* Writes given data to into the given fos.
*
* @param fos The FileOutputStream to write to.
* @param byte[] The byte[] to write.
*/
private static void writeImageData(FileOutputStream fos, final byte[] data) throws IOException {
fos.write(data);
} }
/** /**
......
...@@ -5,7 +5,11 @@ ...@@ -5,7 +5,11 @@
package org.chromium.chrome.browser.share; package org.chromium.chrome.browser.share;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.net.Uri; import android.net.Uri;
import android.os.Environment;
import android.support.test.filters.SmallTest; import android.support.test.filters.SmallTest;
import android.support.v4.content.FileProvider; import android.support.v4.content.FileProvider;
...@@ -82,6 +86,7 @@ public class ShareImageFileUtilsTest { ...@@ -82,6 +86,7 @@ public class ShareImageFileUtilsTest {
public void setUp() { public void setUp() {
mActivityTestRule.startMainActivityFromLauncher(); mActivityTestRule.startMainActivityFromLauncher();
ContentUriUtils.setFileProviderUtil(new FileProviderHelper()); ContentUriUtils.setFileProviderUtil(new FileProviderHelper());
clearExternalStorageDir();
} }
@After @After
...@@ -135,6 +140,17 @@ public class ShareImageFileUtilsTest { ...@@ -135,6 +140,17 @@ public class ShareImageFileUtilsTest {
runnableHelper.waitForCallback(0, 1, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); runnableHelper.waitForCallback(0, 1, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} }
public void clearExternalStorageDir() {
AsyncTask.SERIAL_EXECUTOR.execute(() -> {
File externalStorageDir = mActivityTestRule.getActivity().getExternalFilesDir(
Environment.DIRECTORY_DOWNLOADS);
String[] children = externalStorageDir.list();
for (int i = 0; i < children.length; i++) {
new File(externalStorageDir, children[i]).delete();
}
});
}
private int fileCountInShareDirectory() throws IOException { private int fileCountInShareDirectory() throws IOException {
return fileCount(ShareImageFileUtils.getSharedFilesDirectory()); return fileCount(ShareImageFileUtils.getSharedFilesDirectory());
} }
...@@ -143,6 +159,17 @@ public class ShareImageFileUtilsTest { ...@@ -143,6 +159,17 @@ public class ShareImageFileUtilsTest {
return filepathExists(ShareImageFileUtils.getSharedFilesDirectory(), fileUri.getPath()); return filepathExists(ShareImageFileUtils.getSharedFilesDirectory(), fileUri.getPath());
} }
private Bitmap getTestBitmap() {
int size = 10;
Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
paint.setColor(android.graphics.Color.GREEN);
canvas.drawRect(0F, 0F, (float) size, (float) size, paint);
return bitmap;
}
@Test @Test
@SmallTest @SmallTest
public void clipboardUriDoNotClearTest() throws TimeoutException, IOException { public void clipboardUriDoNotClearTest() throws TimeoutException, IOException {
...@@ -168,4 +195,46 @@ public class ShareImageFileUtilsTest { ...@@ -168,4 +195,46 @@ public class ShareImageFileUtilsTest {
clearSharedImages(); clearSharedImages();
Assert.assertEquals(0, fileCountInShareDirectory()); Assert.assertEquals(0, fileCountInShareDirectory());
} }
@Test
@SmallTest
public void testSaveBitmap() throws IOException {
String fileName = "chrome-test-bitmap";
ShareImageFileUtils.OnImageSaveListener listener =
new ShareImageFileUtils.OnImageSaveListener() {
@Override
public void onImageSaved(File imageFile) {
AsyncTask.SERIAL_EXECUTOR.execute(() -> {
Assert.assertTrue(imageFile.exists());
Assert.assertTrue(imageFile.isFile());
Assert.assertTrue(imageFile.getPath().contains(fileName));
});
}
@Override
public void onImageSaveError() {
Assert.fail();
}
};
ShareImageFileUtils.saveBitmapToExternalStorage(
mActivityTestRule.getActivity(), fileName, getTestBitmap(), listener);
}
@Test
@SmallTest
public void testGetNextAvailableFile() throws IOException {
String filename = "chrome-test-bitmap";
String extension = ".jpg";
File externalStorageDir = mActivityTestRule.getActivity().getExternalFilesDir(
Environment.DIRECTORY_DOWNLOADS);
File imageFile = ShareImageFileUtils.getNextAvailableFile(
externalStorageDir.getPath(), filename, extension);
Assert.assertTrue(imageFile.exists());
File imageFile2 = ShareImageFileUtils.getNextAvailableFile(
externalStorageDir.getPath(), filename, extension);
Assert.assertTrue(imageFile2.exists());
Assert.assertNotEquals(imageFile.getPath(), imageFile2.getPath());
}
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment