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