1 /*
2  * Copyright (C) 2018 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 android.provider.cts;
18 
19 import static android.provider.cts.ProviderTestUtils.hash;
20 import static android.provider.cts.ProviderTestUtils.resolveVolumeName;
21 
22 import static org.junit.Assert.assertArrayEquals;
23 import static org.junit.Assert.assertEquals;
24 import static org.junit.Assert.assertNotNull;
25 
26 import android.content.ContentResolver;
27 import android.content.ContentUris;
28 import android.content.ContentValues;
29 import android.content.Context;
30 import android.database.ContentObserver;
31 import android.database.Cursor;
32 import android.net.Uri;
33 import android.os.Environment;
34 import android.os.FileUtils;
35 import android.platform.test.annotations.Presubmit;
36 import android.provider.MediaStore;
37 import android.provider.MediaStore.Downloads;
38 import android.provider.MediaStore.Files;
39 import android.provider.MediaStore.Images;
40 import android.provider.cts.MediaStoreUtils.PendingParams;
41 import android.provider.cts.MediaStoreUtils.PendingSession;
42 import android.util.Log;
43 
44 import androidx.test.InstrumentationRegistry;
45 
46 import org.junit.Assume;
47 import org.junit.Before;
48 import org.junit.Test;
49 import org.junit.runner.RunWith;
50 import org.junit.runners.Parameterized;
51 import org.junit.runners.Parameterized.Parameter;
52 import org.junit.runners.Parameterized.Parameters;
53 
54 import java.io.ByteArrayOutputStream;
55 import java.io.File;
56 import java.io.FileInputStream;
57 import java.io.FileOutputStream;
58 import java.io.InputStream;
59 import java.io.OutputStream;
60 import java.io.PrintWriter;
61 import java.nio.charset.StandardCharsets;
62 import java.util.concurrent.CountDownLatch;
63 import java.util.concurrent.TimeUnit;
64 
65 @Presubmit
66 @RunWith(Parameterized.class)
67 public class MediaStore_DownloadsTest {
68     private static final String TAG = MediaStore_DownloadsTest.class.getSimpleName();
69     private static final long NOTIFY_TIMEOUT_MILLIS = 4000;
70 
71     private Context mContext;
72     private ContentResolver mContentResolver;
73     private File mDownloadsDir;
74     private File mPicturesDir;
75     private CountDownLatch mCountDownLatch;
76     private int mInitialDownloadsCount;
77 
78     private Uri mExternalImages;
79     private Uri mExternalDownloads;
80 
81     @Parameter(0)
82     public String mVolumeName;
83 
84     @Parameters
data()85     public static Iterable<? extends Object> data() {
86         return ProviderTestUtils.getSharedVolumeNames();
87     }
88 
89     @Before
setUp()90     public void setUp() throws Exception {
91         mContext = InstrumentationRegistry.getTargetContext();
92         mContentResolver = mContext.getContentResolver();
93 
94         Log.d(TAG, "Using volume " + mVolumeName);
95         mExternalImages = MediaStore.Images.Media.getContentUri(mVolumeName);
96         mExternalDownloads = MediaStore.Downloads.getContentUri(mVolumeName);
97 
98         mDownloadsDir = new File(MediaStore.getVolumePath(resolveVolumeName(mVolumeName)),
99                 Environment.DIRECTORY_DOWNLOADS);
100         mPicturesDir = new File(MediaStore.getVolumePath(resolveVolumeName(mVolumeName)),
101                 Environment.DIRECTORY_PICTURES);
102         mDownloadsDir.mkdirs();
103         mPicturesDir.mkdirs();
104         mInitialDownloadsCount = getInitialDownloadsCount();
105     }
106 
107     @Test
testScannedDownload()108     public void testScannedDownload() throws Exception {
109         Assume.assumeTrue(MediaStore.VOLUME_EXTERNAL.equals(mVolumeName)
110                 || MediaStore.VOLUME_EXTERNAL_PRIMARY.equals(mVolumeName));
111 
112         final File downloadFile = new File(mDownloadsDir, "colors.txt");
113         downloadFile.createNewFile();
114         final String fileContents = "RED;GREEN;BLUE";
115         try (final PrintWriter pw = new PrintWriter(downloadFile)) {
116             pw.print(fileContents);
117         }
118         verifyScannedDownload(downloadFile);
119     }
120 
121     @Test
testScannedMediaDownload()122     public void testScannedMediaDownload() throws Exception {
123         Assume.assumeTrue(MediaStore.VOLUME_EXTERNAL.equals(mVolumeName)
124                 || MediaStore.VOLUME_EXTERNAL_PRIMARY.equals(mVolumeName));
125 
126         final File downloadFile = new File(mDownloadsDir, "scenery.png");
127         downloadFile.createNewFile();
128         try (InputStream in = mContext.getResources().openRawResource(R.raw.scenery);
129                 OutputStream out = new FileOutputStream(downloadFile)) {
130             FileUtils.copy(in, out);
131         }
132         verifyScannedDownload(downloadFile);
133     }
134 
135     @Test
testGetContentUri()136     public void testGetContentUri() throws Exception {
137         Cursor c;
138         assertNotNull(c = mContentResolver.query(mExternalDownloads,
139                 null, null, null, null));
140         c.close();
141     }
142 
143     @Test
testMediaInDownloadsDir()144     public void testMediaInDownloadsDir() throws Exception {
145         Assume.assumeTrue(MediaStore.VOLUME_EXTERNAL.equals(mVolumeName)
146                 || MediaStore.VOLUME_EXTERNAL_PRIMARY.equals(mVolumeName));
147 
148         final String displayName = "cts" + System.nanoTime();
149         final Uri insertUri = insertImage(displayName, "test image",
150                 new File(mDownloadsDir, displayName + ".jpg"), "image/jpeg", R.raw.scenery);
151         final String displayName2 = "cts" + System.nanoTime();
152         final Uri insertUri2 = insertImage(displayName2, "test image2",
153                 new File(mPicturesDir, displayName2 + ".jpg"), "image/jpeg", R.raw.volantis);
154 
155         try (Cursor cursor = mContentResolver.query(mExternalDownloads,
156                 null, "title LIKE ?1", new String[] { displayName }, null)) {
157             assertEquals(1, cursor.getCount());
158             cursor.moveToNext();
159             assertEquals("image/jpeg",
160                     cursor.getString(cursor.getColumnIndex(Images.Media.MIME_TYPE)));
161         }
162 
163         assertEquals(1, mContentResolver.delete(insertUri, null, null));
164         try (Cursor cursor = mContentResolver.query(mExternalDownloads,
165                 null, null, null, null)) {
166             assertEquals(mInitialDownloadsCount, cursor.getCount());
167         }
168     }
169 
170     @Test
testInsertDownload()171     public void testInsertDownload() throws Exception {
172         final String content = "<html><body>Content</body></html>";
173         final String displayName = "cts" + System.nanoTime();
174         final String mimeType = "text/html";
175         final Uri downloadUri = Uri.parse("https://developer.android.com/overview.html");
176         final Uri refererUri = Uri.parse("https://www.android.com");
177 
178         final PendingParams params = new PendingParams(
179                 mExternalDownloads, displayName, mimeType);
180         params.setDownloadUri(downloadUri);
181         params.setRefererUri(refererUri);
182 
183         final Uri pendingUri = MediaStoreUtils.createPending(mContext, params);
184         assertNotNull(pendingUri);
185         final Uri publishUri;
186         try (PendingSession session = MediaStoreUtils.openPending(mContext, pendingUri)) {
187             try (PrintWriter pw = new PrintWriter(session.openOutputStream())) {
188                 pw.print(content);
189             }
190             try (OutputStream out = session.openOutputStream()) {
191                 out.write(content.getBytes(StandardCharsets.UTF_8));
192             }
193             publishUri = session.publish();
194         }
195 
196         try (Cursor cursor = mContentResolver.query(publishUri, null, null, null, null)) {
197             assertEquals(1, cursor.getCount());
198 
199             cursor.moveToNext();
200             assertEquals(mimeType,
201                     cursor.getString(cursor.getColumnIndex(Downloads.MIME_TYPE)));
202             assertEquals(displayName + ".html",
203                     cursor.getString(cursor.getColumnIndex(Downloads.DISPLAY_NAME)));
204             assertEquals(downloadUri.toString(),
205                     cursor.getString(cursor.getColumnIndex(Downloads.DOWNLOAD_URI)));
206             assertEquals(refererUri.toString(),
207                     cursor.getString(cursor.getColumnIndex(Downloads.REFERER_URI)));
208         }
209 
210         final ByteArrayOutputStream actual = new ByteArrayOutputStream();
211         try (InputStream in = mContentResolver.openInputStream(publishUri)) {
212             final byte[] buf = new byte[512];
213             int bytesRead;
214             while ((bytesRead = in.read(buf)) != -1) {
215                 actual.write(buf, 0, bytesRead);
216             }
217         }
218         assertEquals(content, actual.toString(StandardCharsets.UTF_8.name()));
219     }
220 
221     @Test
testUpdateDownload()222     public void testUpdateDownload() throws Exception {
223         final String displayName = "cts" + System.nanoTime();
224         final PendingParams params = new PendingParams(
225                 mExternalDownloads, displayName, "video/3gpp");
226         final Uri downloadUri = Uri.parse("https://www.android.com/download?file=testvideo.3gp");
227         params.setDownloadUri(downloadUri);
228 
229         final Uri pendingUri = MediaStoreUtils.createPending(mContext, params);
230         assertNotNull(pendingUri);
231         final Uri publishUri;
232         try (PendingSession session = MediaStoreUtils.openPending(mContext, pendingUri)) {
233             try (InputStream in = mContext.getResources().openRawResource(R.raw.testvideo);
234                  OutputStream out = session.openOutputStream()) {
235                 android.os.FileUtils.copy(in, out);
236             }
237             publishUri = session.publish();
238         }
239 
240         final ContentValues updateValues = new ContentValues();
241         updateValues.put(MediaStore.Files.FileColumns.MEDIA_TYPE,
242                 MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO);
243         updateValues.put(Downloads.MIME_TYPE, "audio/3gpp");
244         assertEquals(1, mContentResolver.update(publishUri, updateValues, null, null));
245 
246         try (Cursor cursor = mContentResolver.query(publishUri,
247                 null, null, null, null)) {
248             assertEquals(1, cursor.getCount());
249             cursor.moveToNext();
250             assertEquals("audio/3gpp",
251                     cursor.getString(cursor.getColumnIndex(Downloads.MIME_TYPE)));
252             assertEquals(downloadUri.toString(),
253                     cursor.getString(cursor.getColumnIndex(Downloads.DOWNLOAD_URI)));
254         }
255     }
256 
257     @Test
testDeleteDownload()258     public void testDeleteDownload() throws Exception {
259         final String displayName = "cts" + System.nanoTime();
260         final PendingParams params = new PendingParams(
261                 mExternalDownloads, displayName, "video/3gp");
262         final Uri downloadUri = Uri.parse("https://www.android.com/download?file=testvideo.3gp");
263         params.setDownloadUri(downloadUri);
264 
265         final Uri pendingUri = MediaStoreUtils.createPending(mContext, params);
266         assertNotNull(pendingUri);
267         final Uri publishUri;
268         try (PendingSession session = MediaStoreUtils.openPending(mContext, pendingUri)) {
269             try (InputStream in = mContext.getResources().openRawResource(R.raw.testvideo);
270                  OutputStream out = session.openOutputStream()) {
271                 android.os.FileUtils.copy(in, out);
272             }
273             publishUri = session.publish();
274         }
275 
276         assertEquals(1, mContentResolver.delete(publishUri, null, null));
277         try (Cursor cursor = mContentResolver.query(mExternalDownloads,
278                 null, null, null, null)) {
279             assertEquals(mInitialDownloadsCount, cursor.getCount());
280         }
281     }
282 
283     @Test
testNotifyChange()284     public void testNotifyChange() throws Exception {
285         final ContentObserver observer = new ContentObserver(null) {
286             @Override
287             public void onChange(boolean selfChange, Uri uri) {
288                 super.onChange(selfChange, uri);
289                 mCountDownLatch.countDown();
290             }
291         };
292         mContentResolver.registerContentObserver(mExternalDownloads, true, observer);
293         mContentResolver.registerContentObserver(MediaStore.AUTHORITY_URI, false, observer);
294         final Uri volumeUri = MediaStore.AUTHORITY_URI.buildUpon()
295                 .appendPath(mVolumeName)
296                 .build();
297         mContentResolver.registerContentObserver(volumeUri, false, observer);
298 
299         mCountDownLatch = new CountDownLatch(1);
300         final String displayName = "cts" + System.nanoTime();
301         final PendingParams params = new PendingParams(
302                 mExternalDownloads, displayName, "video/3gp");
303         final Uri downloadUri = Uri.parse("https://www.android.com/download?file=testvideo.3gp");
304         params.setDownloadUri(downloadUri);
305         final Uri pendingUri = MediaStoreUtils.createPending(mContext, params);
306         assertNotNull(pendingUri);
307         final Uri publishUri;
308         try (PendingSession session = MediaStoreUtils.openPending(mContext, pendingUri)) {
309             try (InputStream in = mContext.getResources().openRawResource(R.raw.testvideo);
310                  OutputStream out = session.openOutputStream()) {
311                 android.os.FileUtils.copy(in, out);
312             }
313             publishUri = session.publish();
314         }
315         mCountDownLatch.await(NOTIFY_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
316 
317         mCountDownLatch = new CountDownLatch(1);
318         final ContentValues updateValues = new ContentValues();
319         updateValues.put(Files.FileColumns.MEDIA_TYPE, Files.FileColumns.MEDIA_TYPE_AUDIO);
320         updateValues.put(Downloads.MIME_TYPE, "audio/3gp");
321         assertEquals(1, mContentResolver.update(publishUri, updateValues, null, null));
322         mCountDownLatch.await(NOTIFY_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
323 
324         mCountDownLatch = new CountDownLatch(1);
325         assertEquals(1, mContentResolver.delete(publishUri, null, null));
326         mCountDownLatch.await(NOTIFY_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
327     }
328 
getInitialDownloadsCount()329     private int getInitialDownloadsCount() {
330         try (Cursor cursor = mContentResolver.query(mExternalDownloads,
331                 null, null, null, null)) {
332             return cursor.getCount();
333         }
334     }
335 
insertImage(String displayName, String description, File file, String mimeType, int resourceId)336     private Uri insertImage(String displayName, String description,
337             File file, String mimeType, int resourceId) throws Exception {
338         file.createNewFile();
339         try (InputStream in = mContext.getResources().openRawResource(resourceId);
340              OutputStream out = new FileOutputStream(file)) {
341             FileUtils.copy(in, out);
342         }
343 
344         final ContentValues values = new ContentValues();
345         values.put(Images.Media.DISPLAY_NAME, displayName);
346         values.put(Images.Media.TITLE, displayName);
347         values.put(Images.Media.DESCRIPTION, description);
348         values.put(Images.Media.DATA, file.getAbsolutePath());
349         values.put(Images.Media.DATE_ADDED, System.currentTimeMillis() / 1000);
350         values.put(Images.Media.DATE_MODIFIED, System.currentTimeMillis() / 1000);
351         values.put(Images.Media.MIME_TYPE, mimeType);
352 
353         final Uri insertUri = mContentResolver.insert(mExternalImages, values);
354         assertNotNull(insertUri);
355         return insertUri;
356     }
357 
verifyScannedDownload(File file)358     private void verifyScannedDownload(File file) throws Exception {
359         final Uri mediaStoreUri = ProviderTestUtils.scanFile(file);
360         Log.e(TAG, "Scanned file " + file.getAbsolutePath() + ": " + mediaStoreUri);
361         assertArrayEquals("File hashes should match for " + file + " and " + mediaStoreUri,
362                 hash(new FileInputStream(file)),
363                 hash(mContentResolver.openInputStream(mediaStoreUri)));
364 
365         // Verify the file is part of downloads collection.
366         final long id = ContentUris.parseId(mediaStoreUri);
367         final Cursor cursor = mContentResolver.query(mExternalDownloads,
368                 null, MediaStore.Downloads._ID + "=" + id, null, null);
369         assertEquals(1, cursor.getCount());
370     }
371 }
372