1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.screenshot; 18 19 import static com.android.systemui.screenshot.ImageExporter.createSystemFileDisplayName; 20 21 import static org.junit.Assert.assertEquals; 22 import static org.junit.Assert.assertFalse; 23 import static org.junit.Assert.assertNotNull; 24 import static org.junit.Assert.assertTrue; 25 26 import static java.nio.charset.StandardCharsets.US_ASCII; 27 28 import android.content.ContentProvider; 29 import android.content.ContentResolver; 30 import android.content.ContentValues; 31 import android.graphics.Bitmap; 32 import android.graphics.Bitmap.CompressFormat; 33 import android.graphics.BitmapFactory; 34 import android.graphics.Canvas; 35 import android.graphics.Color; 36 import android.graphics.Paint; 37 import android.net.Uri; 38 import android.os.Build; 39 import android.os.ParcelFileDescriptor; 40 import android.os.Process; 41 import android.os.UserHandle; 42 import android.provider.MediaStore; 43 import android.testing.AndroidTestingRunner; 44 import android.view.Display; 45 46 import androidx.exifinterface.media.ExifInterface; 47 import androidx.test.filters.MediumTest; 48 49 import com.android.systemui.SysuiTestCase; 50 51 import com.google.common.util.concurrent.ListenableFuture; 52 53 import org.junit.Before; 54 import org.junit.Test; 55 import org.junit.runner.RunWith; 56 import org.mockito.ArgumentCaptor; 57 import org.mockito.Mock; 58 import org.mockito.Mockito; 59 import org.mockito.MockitoAnnotations; 60 61 import java.io.ByteArrayInputStream; 62 import java.io.IOException; 63 import java.io.InputStream; 64 import java.time.LocalDateTime; 65 import java.time.ZoneId; 66 import java.time.ZonedDateTime; 67 import java.util.UUID; 68 import java.util.concurrent.ExecutionException; 69 import java.util.concurrent.Executor; 70 71 @RunWith(AndroidTestingRunner.class) 72 @MediumTest // file I/O 73 public class ImageExporterTest extends SysuiTestCase { 74 /** Executes directly in the caller's thread */ 75 private static final Executor DIRECT_EXECUTOR = Runnable::run; 76 private static final byte[] EXIF_FILE_TAG = "Exif\u0000\u0000".getBytes(US_ASCII); 77 78 private static final ZonedDateTime CAPTURE_TIME = 79 ZonedDateTime.of(LocalDateTime.of(2020, 12, 15, 13, 15), ZoneId.of("America/New_York")); 80 81 @Mock 82 private ContentResolver mMockContentResolver; 83 84 @Before setup()85 public void setup() { 86 MockitoAnnotations.initMocks(this); 87 } 88 89 @Test testImageFilename()90 public void testImageFilename() { 91 assertEquals("image file name", "Screenshot_20201215-131500.png", 92 ImageExporter.createFilename(CAPTURE_TIME, CompressFormat.PNG, 93 Display.DEFAULT_DISPLAY)); 94 } 95 96 @Test testImageFilename_secondaryDisplay1()97 public void testImageFilename_secondaryDisplay1() { 98 assertEquals("image file name", "Screenshot_20201215-131500-display-1.png", 99 ImageExporter.createFilename(CAPTURE_TIME, CompressFormat.PNG, /* displayId= */ 1)); 100 } 101 102 @Test testImageFilename_secondaryDisplay2()103 public void testImageFilename_secondaryDisplay2() { 104 assertEquals("image file name", "Screenshot_20201215-131500-display-2.png", 105 ImageExporter.createFilename(CAPTURE_TIME, CompressFormat.PNG, /* displayId= */ 2)); 106 } 107 108 @Test testUpdateExifAttributes_timeZoneUTC()109 public void testUpdateExifAttributes_timeZoneUTC() throws IOException { 110 ExifInterface exifInterface = new ExifInterface(new ByteArrayInputStream(EXIF_FILE_TAG), 111 ExifInterface.STREAM_TYPE_EXIF_DATA_ONLY); 112 ImageExporter.updateExifAttributes(exifInterface, 113 UUID.fromString("3c11da99-9284-4863-b1d5-6f3684976814"), 100, 100, 114 ZonedDateTime.of(LocalDateTime.of(2020, 12, 15, 18, 15), ZoneId.of("UTC"))); 115 116 assertEquals("Exif " + ExifInterface.TAG_IMAGE_UNIQUE_ID, 117 "3c11da99-9284-4863-b1d5-6f3684976814", 118 exifInterface.getAttribute(ExifInterface.TAG_IMAGE_UNIQUE_ID)); 119 assertEquals("Exif " + ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "+00:00", 120 exifInterface.getAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL)); 121 } 122 123 @Test testImageExport()124 public void testImageExport() throws ExecutionException, InterruptedException, IOException { 125 ContentResolver contentResolver = mContext.getContentResolver(); 126 ImageExporter exporter = new ImageExporter(contentResolver); 127 128 UUID requestId = UUID.fromString("3c11da99-9284-4863-b1d5-6f3684976814"); 129 Bitmap original = createCheckerBitmap(10, 10, 10); 130 131 ListenableFuture<ImageExporter.Result> direct = 132 exporter.export(DIRECT_EXECUTOR, requestId, original, CAPTURE_TIME, 133 Process.myUserHandle(), Display.DEFAULT_DISPLAY); 134 assertTrue("future should be done", direct.isDone()); 135 assertFalse("future should not be canceled", direct.isCancelled()); 136 ImageExporter.Result result = direct.get(); 137 138 assertEquals("Result should contain the same request id", requestId, result.requestId); 139 assertEquals("Filename should contain the correct filename", 140 "Screenshot_20201215-131500.png", result.fileName); 141 assertNotNull("CompressFormat should be set", result.format); 142 assertEquals("The default CompressFormat should be PNG", CompressFormat.PNG, result.format); 143 assertNotNull("Uri should not be null", result.uri); 144 assertEquals("Timestamp should match input", CAPTURE_TIME.toInstant().toEpochMilli(), 145 result.timestamp); 146 147 Bitmap decoded = null; 148 try (InputStream in = contentResolver.openInputStream(result.uri)) { 149 decoded = BitmapFactory.decodeStream(in); 150 assertNotNull("decoded image should not be null", decoded); 151 assertTrue("original and decoded image should be identical", original.sameAs(decoded)); 152 153 try (ParcelFileDescriptor pfd = contentResolver.openFile(result.uri, "r", null)) { 154 assertNotNull(pfd); 155 ExifInterface exifInterface = new ExifInterface(pfd.getFileDescriptor()); 156 157 assertEquals("Exif " + ExifInterface.TAG_IMAGE_UNIQUE_ID, 158 "3c11da99-9284-4863-b1d5-6f3684976814", 159 exifInterface.getAttribute(ExifInterface.TAG_IMAGE_UNIQUE_ID)); 160 161 assertEquals("Exif " + ExifInterface.TAG_SOFTWARE, "Android " + Build.DISPLAY, 162 exifInterface.getAttribute(ExifInterface.TAG_SOFTWARE)); 163 164 assertEquals("Exif " + ExifInterface.TAG_IMAGE_WIDTH, 100, 165 exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0)); 166 assertEquals("Exif " + ExifInterface.TAG_IMAGE_LENGTH, 100, 167 exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0)); 168 169 assertEquals("Exif " + ExifInterface.TAG_DATETIME_ORIGINAL, "2020:12:15 13:15:00", 170 exifInterface.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL)); 171 assertEquals("Exif " + ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, "000", 172 exifInterface.getAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL)); 173 assertEquals("Exif " + ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "-05:00", 174 exifInterface.getAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL)); 175 } 176 } finally { 177 if (decoded != null) { 178 decoded.recycle(); 179 } 180 contentResolver.delete(result.uri, null); 181 } 182 } 183 184 @Test testImageExport_customizedFileName()185 public void testImageExport_customizedFileName() 186 throws ExecutionException, InterruptedException { 187 // This test only asserts the file name for the case when user specifies a file name, 188 // instead of using the auto-generated name by ImageExporter::createFileName. Other 189 // metadata are not affected by the specified file name. 190 final String customizedFileName = "customized_file_name"; 191 ContentResolver contentResolver = mContext.getContentResolver(); 192 ImageExporter exporter = new ImageExporter(contentResolver); 193 194 UUID requestId = UUID.fromString("3c11da99-9284-4863-b1d5-6f3684976814"); 195 Bitmap original = createCheckerBitmap(10, 10, 10); 196 197 ListenableFuture<ImageExporter.Result> direct = 198 exporter.export(DIRECT_EXECUTOR, requestId, original, CAPTURE_TIME, 199 Process.myUserHandle(), customizedFileName); 200 assertTrue("future should be done", direct.isDone()); 201 assertFalse("future should not be canceled", direct.isCancelled()); 202 ImageExporter.Result result = direct.get(); 203 assertEquals("Filename should contain the correct filename", 204 createSystemFileDisplayName(customizedFileName, CompressFormat.PNG), 205 result.fileName); 206 } 207 208 @Test testMediaStoreMetadata()209 public void testMediaStoreMetadata() { 210 String name = ImageExporter.createFilename(CAPTURE_TIME, CompressFormat.PNG, 211 Display.DEFAULT_DISPLAY); 212 ContentValues values = ImageExporter.createMetadata(CAPTURE_TIME, CompressFormat.PNG, name); 213 assertEquals("Pictures/Screenshots", 214 values.getAsString(MediaStore.MediaColumns.RELATIVE_PATH)); 215 assertEquals("Screenshot_20201215-131500.png", 216 values.getAsString(MediaStore.MediaColumns.DISPLAY_NAME)); 217 assertEquals("image/png", values.getAsString(MediaStore.MediaColumns.MIME_TYPE)); 218 assertEquals(Long.valueOf(1608056100L), 219 values.getAsLong(MediaStore.MediaColumns.DATE_ADDED)); 220 assertEquals(Long.valueOf(1608056100L), 221 values.getAsLong(MediaStore.MediaColumns.DATE_MODIFIED)); 222 assertEquals(Integer.valueOf(1), values.getAsInteger(MediaStore.MediaColumns.IS_PENDING)); 223 assertEquals(Long.valueOf(1608056100L + 86400L), // +1 day 224 values.getAsLong(MediaStore.MediaColumns.DATE_EXPIRES)); 225 } 226 227 @Test testSetUser()228 public void testSetUser() { 229 ImageExporter exporter = new ImageExporter(mMockContentResolver); 230 231 UserHandle imageUserHande = UserHandle.of(10); 232 233 ArgumentCaptor<Uri> uriCaptor = ArgumentCaptor.forClass(Uri.class); 234 // Capture the URI and then return null to bail out of export. 235 Mockito.when(mMockContentResolver.insert(uriCaptor.capture(), Mockito.any())).thenReturn( 236 null); 237 exporter.export(DIRECT_EXECUTOR, UUID.fromString("3c11da99-9284-4863-b1d5-6f3684976814"), 238 null, CAPTURE_TIME, imageUserHande, Display.DEFAULT_DISPLAY); 239 240 Uri expected = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; 241 expected = ContentProvider.maybeAddUserId(expected, imageUserHande.getIdentifier()); 242 243 assertEquals(expected, uriCaptor.getValue()); 244 } 245 246 @SuppressWarnings("SameParameterValue") createCheckerBitmap(int tileSize, int w, int h)247 private Bitmap createCheckerBitmap(int tileSize, int w, int h) { 248 Bitmap bitmap = Bitmap.createBitmap(w * tileSize, h * tileSize, Bitmap.Config.ARGB_8888); 249 Canvas c = new Canvas(bitmap); 250 Paint paint = new Paint(); 251 paint.setStyle(Paint.Style.FILL); 252 253 for (int i = 0; i < h; i++) { 254 int top = i * tileSize; 255 for (int j = 0; j < w; j++) { 256 int left = j * tileSize; 257 paint.setColor(paint.getColor() == Color.WHITE ? Color.BLACK : Color.WHITE); 258 c.drawRect(left, top, left + tileSize, top + tileSize, paint); 259 } 260 } 261 return bitmap; 262 } 263 } 264