Screenshots: ImageExporter
Performs the same image writing steps as SaveImageInBackgroundTask but with only image export functions, packaged in a neatly composable and testable form. Bug: 175744393 Test: atest SystemUITests:com.android.systemui.screenshot.ImageExporterTest Change-Id: Ifbdf53daaeaf366da6ae5a25b4756e289172ab66
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 com.android.systemui.screenshot;
|
||||
|
||||
import static android.os.FileUtils.closeQuietly;
|
||||
|
||||
import android.annotation.IntRange;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Bitmap.CompressFormat;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.SystemClock;
|
||||
import android.os.Trace;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.concurrent.futures.CallbackToFutureAdapter;
|
||||
import androidx.exifinterface.media.ExifInterface;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
class ImageExporter {
|
||||
private static final String TAG = LogConfig.logTag(ImageExporter.class);
|
||||
|
||||
static final Duration PENDING_ENTRY_TTL = Duration.ofHours(24);
|
||||
|
||||
// ex: 'Screenshot_20201215-090626.png'
|
||||
private static final String FILENAME_PATTERN = "Screenshot_%1$tY%<tm%<td-%<tH%<tM%<tS.%2$s";
|
||||
private static final String SCREENSHOTS_PATH = Environment.DIRECTORY_PICTURES
|
||||
+ File.separator + Environment.DIRECTORY_SCREENSHOTS;
|
||||
|
||||
private static final String RESOLVER_INSERT_RETURNED_NULL =
|
||||
"ContentResolver#insert returned null.";
|
||||
private static final String RESOLVER_OPEN_FILE_RETURNED_NULL =
|
||||
"ContentResolver#openFile returned null.";
|
||||
private static final String RESOLVER_OPEN_FILE_EXCEPTION =
|
||||
"ContentResolver#openFile threw an exception.";
|
||||
private static final String OPEN_OUTPUT_STREAM_EXCEPTION =
|
||||
"ContentResolver#openOutputStream threw an exception.";
|
||||
private static final String EXIF_READ_EXCEPTION =
|
||||
"ExifInterface threw an exception reading from the file descriptor.";
|
||||
private static final String EXIF_WRITE_EXCEPTION =
|
||||
"ExifInterface threw an exception writing to the file descriptor.";
|
||||
private static final String RESOLVER_UPDATE_ZERO_ROWS =
|
||||
"Failed to publishEntry. ContentResolver#update reported no rows updated.";
|
||||
private static final String IMAGE_COMPRESS_RETURNED_FALSE =
|
||||
"Bitmap.compress returned false. (Failure unknown)";
|
||||
|
||||
private final ContentResolver mResolver;
|
||||
private CompressFormat mCompressFormat = CompressFormat.PNG;
|
||||
private int mQuality = 100;
|
||||
|
||||
@Inject
|
||||
ImageExporter(ContentResolver resolver) {
|
||||
mResolver = resolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the output image format. This also determines extension of the filename created. The
|
||||
* default is {@link CompressFormat#PNG PNG}.
|
||||
*
|
||||
* @see CompressFormat
|
||||
*
|
||||
* @param format the image format for export
|
||||
*/
|
||||
void setFormat(CompressFormat format) {
|
||||
mCompressFormat = format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the quality format. The exact meaning is dependent on the {@link CompressFormat} used.
|
||||
*
|
||||
* @param quality the 'quality' level between 0 and 100
|
||||
*/
|
||||
void setQuality(@IntRange(from = 0, to = 100) int quality) {
|
||||
mQuality = quality;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the image using the given executor.
|
||||
*
|
||||
* @param executor the thread for execution
|
||||
* @param bitmap the bitmap to export
|
||||
*
|
||||
* @return a listenable future result
|
||||
*/
|
||||
ListenableFuture<Uri> export(Executor executor, Bitmap bitmap) {
|
||||
return export(executor, bitmap, ZonedDateTime.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the image using the given executor.
|
||||
*
|
||||
* @param executor the thread for execution
|
||||
* @param bitmap the bitmap to export
|
||||
*
|
||||
* @return a listenable future result
|
||||
*/
|
||||
ListenableFuture<Uri> export(Executor executor, Bitmap bitmap, ZonedDateTime captureTime) {
|
||||
final Task task = new Task(mResolver, bitmap, captureTime, mCompressFormat, mQuality);
|
||||
return CallbackToFutureAdapter.getFuture(
|
||||
(completer) -> {
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
completer.set(task.execute());
|
||||
} catch (ImageExportException | InterruptedException e) {
|
||||
completer.setException(e);
|
||||
}
|
||||
});
|
||||
return task;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static class Task {
|
||||
private final ContentResolver mResolver;
|
||||
private final ZonedDateTime mCaptureTime;
|
||||
private final CompressFormat mFormat;
|
||||
private final int mQuality;
|
||||
private final Bitmap mBitmap;
|
||||
|
||||
Task(ContentResolver resolver, Bitmap bitmap, ZonedDateTime captureTime,
|
||||
CompressFormat format, int quality) {
|
||||
mResolver = resolver;
|
||||
mBitmap = bitmap;
|
||||
mCaptureTime = captureTime;
|
||||
mFormat = format;
|
||||
mQuality = quality;
|
||||
}
|
||||
|
||||
public Uri execute() throws ImageExportException, InterruptedException {
|
||||
Trace.beginSection("ImageExporter_execute");
|
||||
Uri uri = null;
|
||||
Instant start = null;
|
||||
try {
|
||||
if (LogConfig.DEBUG_STORAGE) {
|
||||
Log.d(TAG, "image export started");
|
||||
start = Instant.now();
|
||||
}
|
||||
uri = createEntry(mFormat, mCaptureTime);
|
||||
throwIfInterrupted();
|
||||
|
||||
writeImage(mBitmap, mFormat, mQuality, uri);
|
||||
throwIfInterrupted();
|
||||
|
||||
writeExif(uri, mBitmap.getWidth(), mBitmap.getHeight(), mCaptureTime);
|
||||
throwIfInterrupted();
|
||||
|
||||
publishEntry(uri);
|
||||
|
||||
if (LogConfig.DEBUG_STORAGE) {
|
||||
Log.d(TAG, "image export completed: "
|
||||
+ Duration.between(start, Instant.now()).toMillis() + " ms");
|
||||
}
|
||||
} catch (ImageExportException e) {
|
||||
if (uri != null) {
|
||||
mResolver.delete(uri, null);
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
Trace.endSection();
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
Uri createEntry(CompressFormat format, ZonedDateTime time) throws ImageExportException {
|
||||
Trace.beginSection("ImageExporter_createEntry");
|
||||
try {
|
||||
final ContentValues values = createMetadata(time, format);
|
||||
Uri uri = mResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
|
||||
if (uri == null) {
|
||||
throw new ImageExportException(RESOLVER_INSERT_RETURNED_NULL);
|
||||
}
|
||||
return uri;
|
||||
} finally {
|
||||
Trace.endSection();
|
||||
}
|
||||
}
|
||||
|
||||
void writeImage(Bitmap bitmap, CompressFormat format, int quality,
|
||||
Uri contentUri) throws ImageExportException {
|
||||
Trace.beginSection("ImageExporter_writeImage");
|
||||
try (OutputStream out = mResolver.openOutputStream(contentUri)) {
|
||||
long start = SystemClock.elapsedRealtime();
|
||||
if (!bitmap.compress(format, quality, out)) {
|
||||
throw new ImageExportException(IMAGE_COMPRESS_RETURNED_FALSE);
|
||||
} else if (LogConfig.DEBUG_STORAGE) {
|
||||
Log.d(TAG, "Bitmap.compress took "
|
||||
+ (SystemClock.elapsedRealtime() - start) + " ms");
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new ImageExportException(OPEN_OUTPUT_STREAM_EXCEPTION, ex);
|
||||
} finally {
|
||||
Trace.endSection();
|
||||
}
|
||||
}
|
||||
|
||||
void writeExif(Uri uri, int width, int height, ZonedDateTime captureTime)
|
||||
throws ImageExportException {
|
||||
Trace.beginSection("ImageExporter_writeExif");
|
||||
ParcelFileDescriptor pfd = null;
|
||||
try {
|
||||
pfd = mResolver.openFile(uri, "rw", null);
|
||||
if (pfd == null) {
|
||||
throw new ImageExportException(RESOLVER_OPEN_FILE_RETURNED_NULL);
|
||||
}
|
||||
ExifInterface exif;
|
||||
try {
|
||||
exif = new ExifInterface(pfd.getFileDescriptor());
|
||||
} catch (IOException e) {
|
||||
throw new ImageExportException(EXIF_READ_EXCEPTION, e);
|
||||
}
|
||||
|
||||
updateExifAttributes(exif, width, height, captureTime);
|
||||
try {
|
||||
exif.saveAttributes();
|
||||
} catch (IOException e) {
|
||||
throw new ImageExportException(EXIF_WRITE_EXCEPTION, e);
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new ImageExportException(RESOLVER_OPEN_FILE_EXCEPTION, e);
|
||||
} finally {
|
||||
closeQuietly(pfd);
|
||||
Trace.endSection();
|
||||
}
|
||||
}
|
||||
|
||||
void publishEntry(Uri uri) throws ImageExportException {
|
||||
Trace.beginSection("ImageExporter_publishEntry");
|
||||
try {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(MediaStore.MediaColumns.IS_PENDING, 0);
|
||||
values.putNull(MediaStore.MediaColumns.DATE_EXPIRES);
|
||||
final int rowsUpdated = mResolver.update(uri, values, /* extras */ null);
|
||||
if (rowsUpdated < 1) {
|
||||
throw new ImageExportException(RESOLVER_UPDATE_ZERO_ROWS);
|
||||
}
|
||||
} finally {
|
||||
Trace.endSection();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "compress [" + mBitmap + "] to [" + mFormat + "] at quality " + mQuality;
|
||||
}
|
||||
}
|
||||
|
||||
static String createFilename(ZonedDateTime time, CompressFormat format) {
|
||||
return String.format(FILENAME_PATTERN, time, fileExtension(format));
|
||||
}
|
||||
|
||||
static ContentValues createMetadata(ZonedDateTime captureTime, CompressFormat format) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(MediaStore.MediaColumns.RELATIVE_PATH, SCREENSHOTS_PATH);
|
||||
values.put(MediaStore.MediaColumns.DISPLAY_NAME, createFilename(captureTime, format));
|
||||
values.put(MediaStore.MediaColumns.MIME_TYPE, getMimeType(format));
|
||||
values.put(MediaStore.MediaColumns.DATE_ADDED, captureTime.toEpochSecond());
|
||||
values.put(MediaStore.MediaColumns.DATE_MODIFIED, captureTime.toEpochSecond());
|
||||
values.put(MediaStore.MediaColumns.DATE_EXPIRES,
|
||||
captureTime.plus(PENDING_ENTRY_TTL).toEpochSecond());
|
||||
values.put(MediaStore.MediaColumns.IS_PENDING, 1);
|
||||
return values;
|
||||
}
|
||||
|
||||
static void updateExifAttributes(ExifInterface exif, int width, int height,
|
||||
ZonedDateTime captureTime) {
|
||||
exif.setAttribute(ExifInterface.TAG_SOFTWARE, "Android " + Build.DISPLAY);
|
||||
exif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, Integer.toString(width));
|
||||
exif.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, Integer.toString(height));
|
||||
|
||||
String dateTime = DateTimeFormatter.ofPattern("yyyy:MM:dd HH:mm:ss").format(captureTime);
|
||||
String subSec = DateTimeFormatter.ofPattern("SSS").format(captureTime);
|
||||
String timeZone = DateTimeFormatter.ofPattern("xxx").format(captureTime);
|
||||
|
||||
exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, dateTime);
|
||||
exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, subSec);
|
||||
exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, timeZone);
|
||||
|
||||
exif.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, dateTime);
|
||||
exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, subSec);
|
||||
exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_DIGITIZED, timeZone);
|
||||
}
|
||||
|
||||
static String getMimeType(CompressFormat format) {
|
||||
switch (format) {
|
||||
case JPEG:
|
||||
return "image/jpeg";
|
||||
case PNG:
|
||||
return "image/png";
|
||||
case WEBP:
|
||||
case WEBP_LOSSLESS:
|
||||
case WEBP_LOSSY:
|
||||
return "image/webp";
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown CompressFormat!");
|
||||
}
|
||||
}
|
||||
|
||||
static String fileExtension(CompressFormat format) {
|
||||
switch (format) {
|
||||
case JPEG:
|
||||
return "jpg";
|
||||
case PNG:
|
||||
return "png";
|
||||
case WEBP:
|
||||
case WEBP_LOSSY:
|
||||
case WEBP_LOSSLESS:
|
||||
return "webp";
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown CompressFormat!");
|
||||
}
|
||||
}
|
||||
|
||||
private static void throwIfInterrupted() throws InterruptedException {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
throw new InterruptedException();
|
||||
}
|
||||
}
|
||||
|
||||
static final class ImageExportException extends IOException {
|
||||
ImageExportException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
ImageExportException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 com.android.systemui.screenshot;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Bitmap.CompressFormat;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.MediaStore;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
|
||||
import androidx.exifinterface.media.ExifInterface;
|
||||
import androidx.test.filters.MediumTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import com.android.systemui.SysuiTestCase;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
@MediumTest // file I/O
|
||||
public class ImageExporterTest extends SysuiTestCase {
|
||||
|
||||
/** Executes directly in the caller's thread */
|
||||
private static final Executor DIRECT_EXECUTOR = Runnable::run;
|
||||
private static final byte[] EXIF_FILE_TAG = "Exif\u0000\u0000".getBytes(US_ASCII);
|
||||
|
||||
private static final ZonedDateTime CAPTURE_TIME =
|
||||
ZonedDateTime.of(LocalDateTime.of(2020, 12, 15, 13, 15), ZoneId.of("EST"));
|
||||
|
||||
@Test
|
||||
public void testImageFilename() {
|
||||
assertEquals("image file name", "Screenshot_20201215-131500.png",
|
||||
ImageExporter.createFilename(CAPTURE_TIME, CompressFormat.PNG));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateExifAttributes_timeZoneUTC() throws IOException {
|
||||
ExifInterface exifInterface = new ExifInterface(new ByteArrayInputStream(EXIF_FILE_TAG),
|
||||
ExifInterface.STREAM_TYPE_EXIF_DATA_ONLY);
|
||||
|
||||
ImageExporter.updateExifAttributes(exifInterface, 100, 100,
|
||||
ZonedDateTime.of(LocalDateTime.of(2020, 12, 15, 18, 15), ZoneId.of("UTC")));
|
||||
|
||||
assertEquals("Exif " + ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "+00:00",
|
||||
exifInterface.getAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL));
|
||||
assertEquals("Exif " + ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "+00:00",
|
||||
exifInterface.getAttribute(ExifInterface.TAG_OFFSET_TIME_DIGITIZED));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImageExport() throws ExecutionException, InterruptedException, IOException {
|
||||
Context context = InstrumentationRegistry.getInstrumentation().getContext();
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
ImageExporter exporter = new ImageExporter(contentResolver);
|
||||
|
||||
Bitmap original = createCheckerBitmap(10, 10, 10);
|
||||
|
||||
ListenableFuture<Uri> direct = exporter.export(DIRECT_EXECUTOR, original, CAPTURE_TIME);
|
||||
assertTrue("future should be done", direct.isDone());
|
||||
assertFalse("future should not be canceled", direct.isCancelled());
|
||||
Uri result = direct.get();
|
||||
|
||||
assertNotNull("Uri should not be null", result);
|
||||
Bitmap decoded = null;
|
||||
try (InputStream in = contentResolver.openInputStream(result)) {
|
||||
decoded = BitmapFactory.decodeStream(in);
|
||||
assertNotNull("decoded image should not be null", decoded);
|
||||
assertTrue("original and decoded image should be identical", original.sameAs(decoded));
|
||||
|
||||
try (ParcelFileDescriptor pfd = contentResolver.openFile(result, "r", null)) {
|
||||
assertNotNull(pfd);
|
||||
ExifInterface exifInterface = new ExifInterface(pfd.getFileDescriptor());
|
||||
|
||||
assertEquals("Exif " + ExifInterface.TAG_SOFTWARE, "Android " + Build.DISPLAY,
|
||||
exifInterface.getAttribute(ExifInterface.TAG_SOFTWARE));
|
||||
|
||||
assertEquals("Exif " + ExifInterface.TAG_IMAGE_WIDTH, 100,
|
||||
exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0));
|
||||
assertEquals("Exif " + ExifInterface.TAG_IMAGE_LENGTH, 100,
|
||||
exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0));
|
||||
|
||||
assertEquals("Exif " + ExifInterface.TAG_DATETIME_ORIGINAL, "2020:12:15 13:15:00",
|
||||
exifInterface.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL));
|
||||
assertEquals("Exif " + ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, "000",
|
||||
exifInterface.getAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL));
|
||||
assertEquals("Exif " + ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "-05:00",
|
||||
exifInterface.getAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL));
|
||||
|
||||
assertEquals("Exif " + ExifInterface.TAG_DATETIME_DIGITIZED, "2020:12:15 13:15:00",
|
||||
exifInterface.getAttribute(ExifInterface.TAG_DATETIME_DIGITIZED));
|
||||
assertEquals("Exif " + ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, "000",
|
||||
exifInterface.getAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED));
|
||||
assertEquals("Exif " + ExifInterface.TAG_OFFSET_TIME_DIGITIZED, "-05:00",
|
||||
exifInterface.getAttribute(ExifInterface.TAG_OFFSET_TIME_DIGITIZED));
|
||||
}
|
||||
} finally {
|
||||
if (decoded != null) {
|
||||
decoded.recycle();
|
||||
}
|
||||
contentResolver.delete(result, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMediaStoreMetadata() {
|
||||
ContentValues values = ImageExporter.createMetadata(CAPTURE_TIME, CompressFormat.PNG);
|
||||
assertEquals("Pictures/Screenshots",
|
||||
values.getAsString(MediaStore.MediaColumns.RELATIVE_PATH));
|
||||
assertEquals("Screenshot_20201215-131500.png",
|
||||
values.getAsString(MediaStore.MediaColumns.DISPLAY_NAME));
|
||||
assertEquals("image/png", values.getAsString(MediaStore.MediaColumns.MIME_TYPE));
|
||||
assertEquals(Long.valueOf(1608056100L),
|
||||
values.getAsLong(MediaStore.MediaColumns.DATE_ADDED));
|
||||
assertEquals(Long.valueOf(1608056100L),
|
||||
values.getAsLong(MediaStore.MediaColumns.DATE_MODIFIED));
|
||||
assertEquals(Integer.valueOf(1), values.getAsInteger(MediaStore.MediaColumns.IS_PENDING));
|
||||
assertEquals(Long.valueOf(1608056100L + 86400L), // +1 day
|
||||
values.getAsLong(MediaStore.MediaColumns.DATE_EXPIRES));
|
||||
}
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private Bitmap createCheckerBitmap(int tileSize, int w, int h) {
|
||||
Bitmap bitmap = Bitmap.createBitmap(w * tileSize, h * tileSize, Bitmap.Config.ARGB_8888);
|
||||
Canvas c = new Canvas(bitmap);
|
||||
Paint paint = new Paint();
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
|
||||
for (int i = 0; i < h; i++) {
|
||||
int top = i * tileSize;
|
||||
for (int j = 0; j < w; j++) {
|
||||
int left = j * tileSize;
|
||||
paint.setColor(paint.getColor() == Color.WHITE ? Color.BLACK : Color.WHITE);
|
||||
c.drawRect(left, top, left + tileSize, top + tileSize, paint);
|
||||
}
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user