1 /*
2  * Copyright (C) 2011 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 package android.app.cts;
17 
18 import static android.Manifest.permission.WRITE_MEDIA_STORAGE;
19 import static android.content.pm.PackageManager.PERMISSION_DENIED;
20 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
21 
22 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
23 
24 import static org.junit.Assert.assertEquals;
25 import static org.junit.Assert.assertFalse;
26 import static org.junit.Assert.assertNotNull;
27 import static org.junit.Assert.assertTrue;
28 import static org.junit.Assert.fail;
29 
30 import android.app.DownloadManager;
31 import android.app.DownloadManager.Query;
32 import android.app.DownloadManager.Request;
33 import android.app.stubs.GetResultActivity;
34 import android.content.Context;
35 import android.content.Intent;
36 import android.content.IntentFilter;
37 import android.content.pm.ApplicationInfo;
38 import android.content.pm.PackageInfo;
39 import android.content.pm.PackageManager;
40 import android.content.pm.ProviderInfo;
41 import android.content.pm.ResolveInfo;
42 import android.database.Cursor;
43 import android.net.Uri;
44 import android.os.Environment;
45 import android.os.FileUtils;
46 import android.os.ParcelFileDescriptor;
47 import android.provider.DocumentsContract;
48 import android.provider.MediaStore;
49 import android.util.Log;
50 import android.util.LongSparseArray;
51 import android.util.Pair;
52 
53 import androidx.test.filters.FlakyTest;
54 import androidx.test.runner.AndroidJUnit4;
55 import androidx.test.uiautomator.UiObject;
56 import androidx.test.uiautomator.UiObjectNotFoundException;
57 import androidx.test.uiautomator.UiSelector;
58 
59 import com.android.compatibility.common.util.CddTest;
60 
61 import org.junit.Assert;
62 import org.junit.Test;
63 import org.junit.runner.RunWith;
64 
65 import java.io.File;
66 import java.io.FileNotFoundException;
67 import java.io.FileOutputStream;
68 import java.io.IOException;
69 import java.io.InputStream;
70 import java.io.OutputStream;
71 
72 @RunWith(AndroidJUnit4.class)
73 public class DownloadManagerTest extends DownloadManagerTestBase {
74     private static final String DOWNLOAD_STORAGE_PROVIDER_AUTHORITY =
75             "com.android.providers.downloads.documents";
76     @FlakyTest
77     @Test
testDownloadManager()78     public void testDownloadManager() throws Exception {
79         final DownloadCompleteReceiver receiver = new DownloadCompleteReceiver();
80         try {
81             IntentFilter intentFilter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
82             mContext.registerReceiver(receiver, intentFilter, Context.RECEIVER_EXPORTED);
83 
84             long goodId = mDownloadManager.enqueue(new Request(getGoodUrl()));
85             long badId = mDownloadManager.enqueue(new Request(getBadUrl()));
86 
87             int allDownloads = getTotalNumberDownloads();
88             assertEquals(2, allDownloads);
89 
90             assertDownloadQueryableById(goodId);
91             assertDownloadQueryableById(badId);
92 
93             receiver.waitForDownloadComplete(SHORT_TIMEOUT, goodId, badId);
94 
95             assertDownloadQueryableByStatus(DownloadManager.STATUS_SUCCESSFUL);
96             assertDownloadQueryableByStatus(DownloadManager.STATUS_FAILED);
97 
98             assertRemoveDownload(goodId, allDownloads - 1);
99             assertRemoveDownload(badId, allDownloads - 2);
100         } finally {
101             mContext.unregisterReceiver(receiver);
102         }
103     }
104 
105     @FlakyTest
106     @Test
testDownloadManagerSupportsHttp()107     public void testDownloadManagerSupportsHttp() throws Exception {
108         final DownloadCompleteReceiver receiver = new DownloadCompleteReceiver();
109         try {
110             IntentFilter intentFilter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
111             mContext.registerReceiver(receiver, intentFilter, Context.RECEIVER_EXPORTED);
112 
113             long id = mDownloadManager.enqueue(new Request(getGoodUrl()));
114 
115             assertEquals(1, getTotalNumberDownloads());
116 
117             assertDownloadQueryableById(id);
118 
119             receiver.waitForDownloadComplete(SHORT_TIMEOUT, id);
120 
121             assertDownloadQueryableByStatus(DownloadManager.STATUS_SUCCESSFUL);
122 
123             assertRemoveDownload(id, 0);
124         } finally {
125             mContext.unregisterReceiver(receiver);
126         }
127     }
128 
129     @FlakyTest
130     @Test
testDownloadManagerSupportsHttpWithExternalWebServer()131     public void testDownloadManagerSupportsHttpWithExternalWebServer() throws Exception {
132         if (!hasInternetConnection()) {
133             Log.i(TAG, "testDownloadManagerSupportsHttpWithExternalWebServer() ignored on device without Internet");
134             return;
135         }
136 
137         // As a result of testDownloadManagerSupportsHttpsWithExternalWebServer relying on an
138         // external resource https://www.example.com this test uses http://www.example.com to help
139         // disambiguate errors from testDownloadManagerSupportsHttpsWithExternalWebServer.
140 
141         final DownloadCompleteReceiver receiver = new DownloadCompleteReceiver();
142         try {
143             IntentFilter intentFilter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
144             mContext.registerReceiver(receiver, intentFilter, Context.RECEIVER_EXPORTED);
145 
146             long id = mDownloadManager.enqueue(new Request(Uri.parse("http://www.example.com")));
147 
148             assertEquals(1, getTotalNumberDownloads());
149 
150             assertDownloadQueryableById(id);
151 
152             receiver.waitForDownloadComplete(LONG_TIMEOUT, id);
153 
154             assertDownloadQueryableByStatus(DownloadManager.STATUS_SUCCESSFUL);
155 
156             assertRemoveDownload(id, 0);
157         } finally {
158             mContext.unregisterReceiver(receiver);
159         }
160     }
161 
162     @Test
testDownloadManagerSupportsHttpsWithExternalWebServer()163     public void testDownloadManagerSupportsHttpsWithExternalWebServer() throws Exception {
164         if (!hasInternetConnection()) {
165             Log.i(TAG, "testDownloadManagerSupportsHttpsWithExternalWebServer() ignored on device without Internet");
166             return;
167         }
168 
169         // For HTTPS, DownloadManager trusts only SSL server certs issued by CAs trusted by the
170         // system. Unfortunately, this means that it cannot trust the mock web server's SSL cert.
171         // Until this is resolved (e.g., by making it possible to specify additional CA certs to
172         // trust for a particular download), this test relies on https://www.example.com being
173         // operational and reachable from the Android under test.
174 
175         final DownloadCompleteReceiver receiver = new DownloadCompleteReceiver();
176         try {
177             IntentFilter intentFilter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
178             mContext.registerReceiver(receiver, intentFilter, Context.RECEIVER_EXPORTED);
179 
180             long id = mDownloadManager.enqueue(new Request(Uri.parse("https://www.example.com")));
181 
182             assertEquals(1, getTotalNumberDownloads());
183 
184             assertDownloadQueryableById(id);
185 
186             receiver.waitForDownloadComplete(LONG_TIMEOUT, id);
187 
188             assertDownloadQueryableByStatus(DownloadManager.STATUS_SUCCESSFUL);
189 
190             assertRemoveDownload(id, 0);
191         } finally {
192             mContext.unregisterReceiver(receiver);
193         }
194     }
195 
196     @CddTest(requirement="7.6.1")
197     @Test
testMinimumDownload()198     public void testMinimumDownload() throws Exception {
199         final DownloadCompleteReceiver receiver = new DownloadCompleteReceiver();
200         try {
201             IntentFilter intentFilter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
202             mContext.registerReceiver(receiver, intentFilter, Context.RECEIVER_EXPORTED);
203 
204             long id = mDownloadManager.enqueue(new Request(getMinimumDownloadUrl()));
205             receiver.waitForDownloadComplete(LONG_TIMEOUT, id);
206 
207             ParcelFileDescriptor fileDescriptor = mDownloadManager.openDownloadedFile(id);
208             assertEquals(MINIMUM_DOWNLOAD_BYTES, fileDescriptor.getStatSize());
209 
210             Cursor cursor = null;
211             try {
212                 cursor = mDownloadManager.query(new Query().setFilterById(id));
213                 assertTrue(cursor.moveToNext());
214                 assertEquals(DownloadManager.STATUS_SUCCESSFUL, cursor.getInt(
215                         cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)));
216                 assertEquals(MINIMUM_DOWNLOAD_BYTES, cursor.getInt(
217                         cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)));
218                 assertFalse(cursor.moveToNext());
219             } finally {
220                 if (cursor != null) {
221                     cursor.close();
222                 }
223             }
224 
225             assertRemoveDownload(id, 0);
226         } finally {
227             mContext.unregisterReceiver(receiver);
228         }
229     }
230 
231     /**
232      * Set download locations and verify that file is downloaded to correct location.
233      *
234      * Checks three different methods of setting location: directly via setDestinationUri, and
235      * indirectly through setDestinationInExternalFilesDir and setDestinationinExternalPublicDir.
236      */
237     @FlakyTest
238     @Test
testDownloadManagerDestination()239     public void testDownloadManagerDestination() throws Exception {
240         File uriLocation = new File(mContext.getExternalFilesDir(null), "uriFile.bin");
241         deleteFromShell(uriLocation);
242 
243         File extFileLocation = new File(mContext.getExternalFilesDir(null), "extFile.bin");
244         deleteFromShell(extFileLocation);
245 
246         File publicLocation = new File(
247                 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
248                 "publicFile.bin");
249         deleteFromShell(publicLocation);
250 
251         final DownloadCompleteReceiver receiver = new DownloadCompleteReceiver();
252         try {
253             IntentFilter intentFilter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
254             mContext.registerReceiver(receiver, intentFilter, Context.RECEIVER_EXPORTED);
255 
256             Request requestUri = new Request(getGoodUrl());
257             requestUri.setDestinationUri(Uri.fromFile(uriLocation));
258             long uriId = mDownloadManager.enqueue(requestUri);
259 
260             Request requestExtFile = new Request(getGoodUrl());
261             requestExtFile.setDestinationInExternalFilesDir(mContext, null, "extFile.bin");
262             long extFileId = mDownloadManager.enqueue(requestExtFile);
263 
264             Request requestPublic = new Request(getGoodUrl());
265             requestPublic.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS,
266                     "publicFile.bin");
267             long publicId = mDownloadManager.enqueue(requestPublic);
268 
269             int allDownloads = getTotalNumberDownloads();
270             assertEquals(3, allDownloads);
271 
272             receiver.waitForDownloadComplete(SHORT_TIMEOUT, uriId, extFileId, publicId);
273 
274             assertSuccessfulDownload(uriId, uriLocation);
275             assertSuccessfulDownload(extFileId, extFileLocation);
276             assertSuccessfulDownload(publicId, publicLocation);
277 
278             assertRemoveDownload(uriId, allDownloads - 1);
279             assertRemoveDownload(extFileId, allDownloads - 2);
280             assertRemoveDownload(publicId, allDownloads - 3);
281         } finally {
282             mContext.unregisterReceiver(receiver);
283         }
284     }
285 
286     /**
287      * Set the download location and verify that the extension of the file name is left unchanged.
288      */
289     @Test
testDownloadManagerDestinationExtension()290     public void testDownloadManagerDestinationExtension() throws Exception {
291         String noExt = "noiseandchirps";
292         File noExtLocation = new File(mContext.getExternalFilesDir(null), noExt);
293         deleteFromShell(noExtLocation);
294 
295         String wrongExt = "noiseandchirps.wrong";
296         File wrongExtLocation = new File(mContext.getExternalFilesDir(null), wrongExt);
297         deleteFromShell(wrongExtLocation);
298 
299         final DownloadCompleteReceiver receiver = new DownloadCompleteReceiver();
300         try {
301             IntentFilter intentFilter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
302             mContext.registerReceiver(receiver, intentFilter, Context.RECEIVER_EXPORTED);
303 
304             Request requestNoExt = new Request(getAssetUrl(noExt));
305             requestNoExt.setDestinationUri(Uri.fromFile(noExtLocation));
306             long noExtId = mDownloadManager.enqueue(requestNoExt);
307 
308             Request requestWrongExt = new Request(getAssetUrl(wrongExt));
309             requestWrongExt.setDestinationUri(Uri.fromFile(wrongExtLocation));
310             long wrongExtId = mDownloadManager.enqueue(requestWrongExt);
311 
312             int allDownloads = getTotalNumberDownloads();
313             assertEquals(2, allDownloads);
314 
315             receiver.waitForDownloadComplete(SHORT_TIMEOUT, noExtId, wrongExtId);
316 
317             assertSuccessfulDownload(noExtId, noExtLocation);
318             assertSuccessfulDownload(wrongExtId, wrongExtLocation);
319 
320             assertRemoveDownload(noExtId, allDownloads - 1);
321             assertRemoveDownload(wrongExtId, allDownloads - 2);
322         } finally {
323             mContext.unregisterReceiver(receiver);
324         }
325     }
326 
327     @Test
testSetDestinationUri_privateAppDir()328     public void testSetDestinationUri_privateAppDir() throws Exception {
329         // Make sure the private app directory exists
330         runShellCommand("mkdir -p /sdcard/Android/data/com.android.shell -m 2770");
331         final File path = new File("/sdcard/Android/data/com.android.shell/"
332                 + TAG + System.currentTimeMillis());
333 
334         final DownloadCompleteReceiver receiver = new DownloadCompleteReceiver();
335         try {
336             IntentFilter intentFilter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
337             mContext.registerReceiver(receiver, intentFilter, Context.RECEIVER_EXPORTED);
338 
339             DownloadManager.Request requestPublic = new DownloadManager.Request(getGoodUrl());
340             requestPublic.setDestinationUri(Uri.fromFile(path));
341             mDownloadManager.enqueue(requestPublic);
342             Assert.fail("Cannot download files into other app's private directories");
343         } catch (SecurityException expected) {
344         } finally {
345             mContext.unregisterReceiver(receiver);
346         }
347     }
348 
349     @Test
testSetDestinationUri_invalidRequests()350     public void testSetDestinationUri_invalidRequests() throws Exception {
351         final File documentsFile = new File(
352                 Environment.getExternalStoragePublicDirectory("TestDir"),
353                 "uriFile.bin");
354         deleteFromShell(documentsFile);
355 
356         final Request badRequest = new Request(getGoodUrl());
357         badRequest.setDestinationUri(Uri.fromFile(documentsFile));
358         try {
359             mDownloadManager.enqueue(badRequest);
360             fail(documentsFile + " is not valid for setDestinationUri()");
361         } catch (Exception e) {
362             // Expected
363         }
364 
365         final Uri badUri = Uri.parse("file:///sdcard/Android/data/"
366                 + mContext.getPackageName() + "/../uriFile.bin");
367         final Request badRequest2 = new Request(getGoodUrl());
368         badRequest2.setDestinationUri(badUri);
369         try {
370             mDownloadManager.enqueue(badRequest2);
371             fail(badUri + " is not valid for setDestinationUri()");
372         } catch (Exception e) {
373             // Expected
374         }
375 
376         final File sdcardPath = new File("/sdcard/uriFile.bin");
377         final Request badRequest3 = new Request(getGoodUrl());
378         badRequest3.setDestinationUri(Uri.fromFile(sdcardPath));
379         try {
380             mDownloadManager.enqueue(badRequest2);
381             fail(sdcardPath + " is not valid for setDestinationUri()");
382         } catch (Exception e) {
383             // Expected
384         }
385     }
386 
387     @Test
testSetDestinationUri()388     public void testSetDestinationUri() throws Exception {
389         final File[] destinationFiles = {
390                 new File(Environment.getExternalStoragePublicDirectory(
391                         Environment.DIRECTORY_DOWNLOADS), System.nanoTime() + "_uriFile.bin"),
392                 new File(Environment.getExternalStoragePublicDirectory(
393                         Environment.DIRECTORY_DOWNLOADS), System.nanoTime() + "_file.jpg"),
394         };
395 
396         for (File downloadsFile : destinationFiles) {
397             final DownloadCompleteReceiver receiver = new DownloadCompleteReceiver();
398             try {
399                 IntentFilter intentFilter = new IntentFilter(
400                         DownloadManager.ACTION_DOWNLOAD_COMPLETE);
401                 mContext.registerReceiver(receiver, intentFilter, Context.RECEIVER_EXPORTED);
402 
403                 DownloadManager.Request request = new DownloadManager.Request(getGoodUrl());
404                 request.setDestinationUri(Uri.fromFile(downloadsFile));
405                 long id = mDownloadManager.enqueue(request);
406 
407                 int allDownloads = getTotalNumberDownloads();
408                 assertEquals(1, allDownloads);
409 
410                 receiver.waitForDownloadComplete(SHORT_TIMEOUT, id);
411                 assertSuccessfulDownload(id, downloadsFile);
412 
413                 assertRemoveDownload(id, 0);
414             } finally {
415                 mContext.unregisterReceiver(receiver);
416             }
417         }
418     }
419 
420     @Test
testSetDestinationInExternalPublicDownloadDir()421     public void testSetDestinationInExternalPublicDownloadDir() throws Exception {
422         final String[] destinations = {
423                 Environment.DIRECTORY_DOWNLOADS,
424                 Environment.DIRECTORY_DCIM,
425         };
426         final String[] subPaths = {
427                 "testing/" + System.nanoTime() + "_publicFile.bin",
428                 System.nanoTime() + "_file.jpg",
429         };
430 
431         for (int i = 0; i < destinations.length; ++i) {
432             final String destination = destinations[i];
433             final String subPath = subPaths[i];
434 
435             final DownloadCompleteReceiver receiver = new DownloadCompleteReceiver();
436             try {
437                 IntentFilter intentFilter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
438                 mContext.registerReceiver(receiver, intentFilter, Context.RECEIVER_EXPORTED);
439 
440                 DownloadManager.Request requestPublic = new DownloadManager.Request(getGoodUrl());
441                 requestPublic.setDestinationInExternalPublicDir(destination, subPath);
442                 long id = mDownloadManager.enqueue(requestPublic);
443 
444                 int allDownloads = getTotalNumberDownloads();
445                 assertEquals(1, allDownloads);
446 
447                 receiver.waitForDownloadComplete(SHORT_TIMEOUT, id);
448                 assertSuccessfulDownload(id, new File(
449                         Environment.getExternalStoragePublicDirectory(destination), subPath));
450 
451                 assertRemoveDownload(id, 0);
452             } finally {
453                 mContext.unregisterReceiver(receiver);
454             }
455         }
456     }
457 
458     @Test
testSetDestinationInExternalPublicDir_invalidRequests()459     public void testSetDestinationInExternalPublicDir_invalidRequests() {
460         final Request badRequest2 = new Request(getGoodUrl());
461         try {
462             badRequest2.setDestinationInExternalPublicDir("TestDir", "uriFile.bin");
463             mDownloadManager.enqueue(badRequest2);
464             fail("/sdcard/TestDir/uriFile.bin"
465                     + " is not valid for setDestinationInExternalPublicDir()");
466         } catch (Exception e) {
467             // Expected
468         }
469     }
470 
canonicalizeProcessName(ApplicationInfo ai)471     private String canonicalizeProcessName(ApplicationInfo ai) {
472         return canonicalizeProcessName(ai.processName, ai);
473     }
474 
canonicalizeProcessName(String process, ApplicationInfo ai)475     private String canonicalizeProcessName(String process, ApplicationInfo ai) {
476         if (process == null) {
477             return null;
478         }
479         // Handle private scoped process names.
480         if (process.startsWith(":")) {
481             return ai.packageName + process;
482         }
483         return process;
484     }
485 
getExternalVolumeMediaStoreFilesCount()486     private int getExternalVolumeMediaStoreFilesCount() {
487         Uri rootUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL);
488         String[] projection = {MediaStore.MediaColumns._ID};
489         int count = 0;
490         try (Cursor cursor = mContext.getContentResolver().query(rootUri, projection, null, null)) {
491             count = cursor.getCount();
492         }
493         return count;
494     }
495 
496     @FlakyTest
497     @Test
testProviderAcceptsCleartext()498     public void testProviderAcceptsCleartext() throws Exception {
499         // Check that all the applications that share an android:process with the DownloadProvider
500         // accept cleartext traffic. Otherwise process loading races can lead to inconsistent flags.
501         final PackageManager pm = mContext.getPackageManager();
502         ProviderInfo downloadInfo = pm.resolveContentProvider("downloads", 0);
503         assertNotNull(downloadInfo);
504         String downloadProcess
505                 = canonicalizeProcessName(downloadInfo.processName, downloadInfo.applicationInfo);
506 
507         for (PackageInfo pi : mContext.getPackageManager().getInstalledPackages(0)) {
508             if (downloadProcess.equals(canonicalizeProcessName(pi.applicationInfo))) {
509                 assertTrue("package: " + pi.applicationInfo.packageName
510                         + " must set android:usesCleartextTraffic=true"
511                         ,(pi.applicationInfo.flags & ApplicationInfo.FLAG_USES_CLEARTEXT_TRAFFIC)
512                         != 0);
513             }
514         }
515     }
516 
517     /**
518      * Test that a download marked as not visible in Downloads ui can be successfully downloaded.
519      */
520     @Test
testDownloadNotVisibleInUi()521     public void testDownloadNotVisibleInUi() throws Exception {
522         File uriLocation = new File(mContext.getExternalFilesDir(null), "uriFile.bin");
523         deleteFromShell(uriLocation);
524 
525         final DownloadCompleteReceiver receiver = new DownloadCompleteReceiver();
526         try {
527             IntentFilter intentFilter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
528             mContext.registerReceiver(receiver, intentFilter, Context.RECEIVER_EXPORTED);
529 
530             final Request request = new Request(getGoodUrl());
531             request.setDestinationUri(Uri.fromFile(uriLocation))
532                     .setVisibleInDownloadsUi(false);
533             long uriId = mDownloadManager.enqueue(request);
534 
535             int allDownloads = getTotalNumberDownloads();
536             assertEquals(1, allDownloads);
537 
538             receiver.waitForDownloadComplete(SHORT_TIMEOUT, uriId);
539 
540             assertSuccessfulDownload(uriId, uriLocation);
541 
542             assertRemoveDownload(uriId, 0);
543         } finally {
544             mContext.unregisterReceiver(receiver);
545         }
546     }
547 
548     //TODO(b/130797842): Remove FlakyTest annotation after that bug is fixed.
549     @FlakyTest
550     @Test
testAddCompletedDownload()551     public void testAddCompletedDownload() throws Exception {
552         final String fileContents = "RED;GREEN;BLUE";
553         final File file = createFile(mContext.getExternalFilesDir(null), "colors.txt");
554 
555         writeToFile(file, fileContents);
556 
557         final long id = mDownloadManager.addCompletedDownload(file.getName(), "Test desc", true,
558                 "text/plain", file.getPath(), fileContents.getBytes().length, true);
559         final String actualContents = readFromFile(mDownloadManager.openDownloadedFile(id));
560         assertEquals(fileContents, actualContents);
561 
562         final Uri downloadUri = mDownloadManager.getUriForDownloadedFile(id);
563         mContext.grantUriPermission("com.android.shell", downloadUri,
564                 Intent.FLAG_GRANT_READ_URI_PERMISSION);
565         final String rawFilePath = getRawFilePath(downloadUri);
566         final String rawFileContents = readFromRawFile(rawFilePath);
567         assertEquals(fileContents, rawFileContents);
568         assertRemoveDownload(id, 0);
569     }
570 
571     @Test
testAddCompletedDownload_downloadDir()572     public void testAddCompletedDownload_downloadDir() throws Exception {
573         final String path = createFile(Environment.getExternalStoragePublicDirectory(
574                         Environment.DIRECTORY_DOWNLOADS), "file1.txt").getPath();
575 
576         final String fileContents = "Test content:" + path + "_" + System.nanoTime();
577 
578         final File file = new File(path);
579         writeToFile(new File(path), fileContents);
580 
581         final long id = mDownloadManager.addCompletedDownload(file.getName(), "Test desc", true,
582                 "text/plain", path, fileContents.getBytes().length, true);
583         final String actualContents = readFromFile(mDownloadManager.openDownloadedFile(id));
584         assertEquals(fileContents, actualContents);
585 
586         final Uri downloadUri = mDownloadManager.getUriForDownloadedFile(id);
587         mContext.grantUriPermission("com.android.shell", downloadUri,
588                 Intent.FLAG_GRANT_READ_URI_PERMISSION);
589         final String rawFilePath = getRawFilePath(downloadUri);
590         final String rawFileContents = readFromRawFile(rawFilePath);
591         assertEquals(fileContents, rawFileContents);
592         assertRemoveDownload(id, 0);
593     }
594 
595     @Test
testAddCompletedDownload_invalidPaths()596     public void testAddCompletedDownload_invalidPaths() throws Exception {
597         final String fileContents = "RED;GREEN;BLUE";
598 
599         // Try adding internal path
600         File file = createFile(mContext.getFilesDir(), "colors.txt");
601         writeToFile(file, fileContents);
602         try {
603             mDownloadManager.addCompletedDownload("Test title", "Test desc", true,
604                     "text/plain", file.getPath(), fileContents.getBytes().length, true);
605             fail("addCompletedDownload should have failed for adding internal path");
606         } catch (Exception e) {
607             // expected
608         }
609 
610         // Try adding path in top-level documents dir
611         file = new File(
612                 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS),
613                 "colors.txt");
614         writeToFileWithDelegator(file, fileContents);
615         try {
616             mDownloadManager.addCompletedDownload("Test title", "Test desc", true,
617                     "text/plain", file.getPath(), fileContents.getBytes().length, true);
618             fail("addCompletedDownload should have failed for top-level download dir");
619         } catch (SecurityException e) {
620             // expected
621         }
622 
623         // Try adding top-level sdcard path
624         final String path = "/sdcard/test-download.txt";
625         writeToFileWithDelegator(new File(path), fileContents);
626         try {
627             mDownloadManager.addCompletedDownload("Test title", "Test desc", true,
628                     "text/plain", path, fileContents.getBytes().length, true);
629             fail("addCompletedDownload should have failed for top-level sdcard path");
630         } catch (SecurityException e) {
631             // expected
632         }
633 
634 
635         final String path2 = "/sdcard/Android/data/" + mContext.getPackageName()
636                 + "/../uriFile.bin";
637         try {
638             mDownloadManager.addCompletedDownload("Test title", "Test desc", true,
639                     "text/plain", path2, fileContents.getBytes().length, true);
640             fail(path2 + " is not valid for addCompleteDownload()");
641         } catch (Exception e) {
642             // Expected
643         }
644 
645         // Try adding non-existent path
646         try {
647             mDownloadManager.addCompletedDownload("Test title", "Test desc", true, "text/plain",
648                     new File(mContext.getExternalFilesDir(null), "test_file.mp4").getPath(),
649                     fileContents.getBytes().length, true);
650             fail("addCompletedDownload should have failed for adding non-existent path");
651         } catch (Exception e) {
652             // expected
653         }
654 
655         // Try adding random string
656         try {
657             mDownloadManager.addCompletedDownload("Test title", "Test desc", true,
658                     "text/plain", "RANDOM", fileContents.getBytes().length, true);
659             fail("addCompletedDownload should have failed for adding random string");
660         } catch (Exception e) {
661             // expected
662         }
663     }
664 
665     @Test
testDownload_mediaScanned()666     public void testDownload_mediaScanned() throws Exception {
667         final String[] destinations = {
668                 Environment.DIRECTORY_MUSIC,
669                 Environment.DIRECTORY_DOWNLOADS,
670         };
671         final String[] subPaths = {
672                 "testmp3.mp3",
673                 "testvideo.3gp",
674         };
675         final Pair<String, String>[] expectedMediaAttributes = new Pair[] {
676                 Pair.create(MediaStore.Audio.AudioColumns.IS_MUSIC, "1"),
677                 Pair.create(MediaStore.Video.VideoColumns.DURATION, "11047"),
678         };
679 
680         for (int i = 0; i < destinations.length; ++i) {
681             final String destination = destinations[i];
682             final String subPath = subPaths[i];
683 
684             final DownloadCompleteReceiver receiver = new DownloadCompleteReceiver();
685             try {
686                 IntentFilter intentFilter = new IntentFilter(
687                         DownloadManager.ACTION_DOWNLOAD_COMPLETE);
688                 mContext.registerReceiver(receiver, intentFilter, Context.RECEIVER_EXPORTED);
689 
690                 DownloadManager.Request requestPublic = new DownloadManager.Request(
691                         getAssetUrl(subPath));
692                 requestPublic.setDestinationInExternalPublicDir(destination, subPath);
693                 long id = mDownloadManager.enqueue(requestPublic);
694 
695                 int allDownloads = getTotalNumberDownloads();
696                 assertEquals(1, allDownloads);
697 
698                 int countBeforeDownload = getExternalVolumeMediaStoreFilesCount();
699                 receiver.waitForDownloadComplete(SHORT_TIMEOUT, id);
700                 assertSuccessfulDownload(id, new File(
701                         Environment.getExternalStoragePublicDirectory(destination), subPath));
702 
703                 int countAfterDownload = getExternalVolumeMediaStoreFilesCount();
704                 // Asserts that only one row entry is added for 1 download
705                 assertEquals((countBeforeDownload + 1), countAfterDownload);
706                 final Uri downloadUri = mDownloadManager.getUriForDownloadedFile(id);
707                 mContext.grantUriPermission("com.android.shell", downloadUri,
708                         Intent.FLAG_GRANT_READ_URI_PERMISSION);
709                 final Uri mediaStoreUri = getMediaStoreUri(downloadUri);
710                 assertEquals(expectedMediaAttributes[i].second,
711                         getMediaStoreColumnValue(mediaStoreUri, expectedMediaAttributes[i].first));
712                 final int expectedSize = getTotalBytes(
713                         mContext.getContentResolver().openInputStream(downloadUri));
714                 assertEquals(expectedSize, Integer.parseInt(getMediaStoreColumnValue(
715                         mediaStoreUri, MediaStore.MediaColumns.SIZE)));
716 
717                 assertRemoveDownload(id, 0);
718             } finally {
719                 mContext.unregisterReceiver(receiver);
720             }
721         }
722     }
723 
724     @Test
testDownload_onMediaStoreDownloadsDeleted()725     public void testDownload_onMediaStoreDownloadsDeleted() throws Exception {
726         assumeDocumentsUiAvailableOnFormFactor();
727 
728         // prepare file
729         File file = new File(Environment.getExternalStoragePublicDirectory(
730                 Environment.DIRECTORY_DOWNLOADS), "cts" + System.nanoTime() + ".mp3");
731         try {
732             stageFile("testmp3.mp3", file);
733             Uri mediaStoreUri = MediaStore.scanFile(mContext.getContentResolver(), file);
734 
735             // setup for activity
736             GetResultActivity activity = setUpForActivity();
737 
738             // call activity as we want uri permission grant on DownloadStorageProvider Uri
739             final Intent intent = new Intent();
740             intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
741             intent.addCategory(Intent.CATEGORY_OPENABLE);
742             intent.setType("*/*");
743             Uri rootUri = DocumentsContract.buildRootUri(DOWNLOAD_STORAGE_PROVIDER_AUTHORITY,
744                     "downloads");
745             intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, rootUri);
746             activity.startActivityForResult(intent, REQUEST_CODE);
747             mDevice.waitForIdle();
748 
749             findDocument(file.getName()).click();
750             final GetResultActivity.Result result = activity.getResult();
751             final Uri uri = result.data.getData();
752 
753             assertTrue(PERMISSION_GRANTED == checkUriPermission(uri));
754 
755             // onMediaStoreDownloadsDeleted API is called on MediaProvider#delete()
756             // for download files. This API revokes grants on DownloadStorageProvider granted Uris
757             LongSparseArray<String> lpa = new LongSparseArray<String>();
758             final long mediaStoreId = Long.parseLong(getMediaStoreColumnValue(mediaStoreUri,
759                     MediaStore.Files.FileColumns._ID));
760             final String mimeType = getMediaStoreColumnValue(mediaStoreUri,
761                     MediaStore.Files.FileColumns.MIME_TYPE);
762             lpa.put(mediaStoreId, mimeType);
763             // This API is permission protected
764             mInstrumentation.getUiAutomation().adoptShellPermissionIdentity(WRITE_MEDIA_STORAGE);
765             try {
766                 mDownloadManager.onMediaStoreDownloadsDeleted(lpa);
767             } finally {
768                 mInstrumentation.getUiAutomation().dropShellPermissionIdentity();
769             }
770 
771             assertTrue(PERMISSION_DENIED == checkUriPermission(uri));
772         } finally {
773             file.delete();
774         }
775     }
776 
setUpForActivity()777     private GetResultActivity setUpForActivity() {
778         final PackageManager pm = mContext.getPackageManager();
779         final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
780         intent.addCategory(Intent.CATEGORY_OPENABLE);
781         intent.setType("*/*");
782         final ResolveInfo ri = pm.resolveActivity(intent, 0);
783         mDocumentsUiPackageId = ri.activityInfo.packageName;
784 
785         final Intent intent2 = new Intent(mContext, GetResultActivity.class);
786         intent2.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
787         GetResultActivity activity = (GetResultActivity) mInstrumentation.startActivitySync(
788                 intent2);
789         mInstrumentation.waitForIdleSync();
790         activity.clearResult();
791         return activity;
792     }
793 
checkUriPermission(Uri uri)794     private int checkUriPermission(Uri uri) {
795         return mContext.checkUriPermission(uri, android.os.Process.myPid(),
796                 android.os.Process.myUid(), Intent.FLAG_GRANT_READ_URI_PERMISSION);
797     }
798 
findDocument(String label)799     private UiObject findDocument(String label) throws UiObjectNotFoundException {
800         final UiSelector docList = new UiSelector().resourceId(getDocumentsUiPackageId()
801                 + ":id/dir_list");
802 
803         // Wait for the first list item to appear
804         assertTrue("First list item",
805                 new UiObject(docList.childSelector(new UiSelector()))
806                         .waitForExists(LONG_TIMEOUT));
807 
808         try {
809             //Enforce to set the list mode
810             //Because UiScrollable can't reach the real bottom (when WEB_LINKABLE_FILE item)
811             // in grid mode when screen landscape mode
812             new UiObject(new UiSelector().resourceId(getDocumentsUiPackageId()
813                     + ":id/sub_menu_list")).click();
814             mDevice.waitForIdle();
815         } catch (UiObjectNotFoundException e) {
816             //do nothing, already be in list mode.
817         }
818 
819         // Repeat swipe gesture to find our item
820         // (UiScrollable#scrollIntoView does not seem to work well with SwipeRefreshLayout)
821         UiObject targetObject = new UiObject(docList.childSelector(new UiSelector().text(label)));
822         UiObject saveButton = findSaveButton();
823         int stepLimit = 10;
824         while (stepLimit-- > 0) {
825             if (targetObject.exists()) {
826                 boolean targetObjectFullyVisible = !saveButton.exists()
827                         || targetObject.getVisibleBounds().bottom
828                         <= saveButton.getVisibleBounds().top;
829                 if (targetObjectFullyVisible) {
830                     break;
831                 }
832             }
833 
834             mDevice.swipe(/* startX= */ mDevice.getDisplayWidth() / 2,
835                     /* startY= */ mDevice.getDisplayHeight() / 2,
836                     /* endX= */ mDevice.getDisplayWidth() / 2,
837                     /* endY= */ 0,
838                     /* steps= */ 40);
839         }
840         return targetObject;
841     }
842 
findSaveButton()843     private UiObject findSaveButton() throws UiObjectNotFoundException {
844         return new UiObject(new UiSelector().resourceId(
845                 getDocumentsUiPackageId() + ":id/container_save")
846                 .childSelector(new UiSelector().resourceId("android:id/button1")));
847     }
848 
getDocumentsUiPackageId()849     private String getDocumentsUiPackageId() {
850         return mDocumentsUiPackageId;
851     }
852 
stageFile(String resId, File file)853     private File stageFile(String resId, File file) throws IOException {
854         // The caller may be trying to stage into a location only available to
855         // the shell user, so we need to perform the entire copy as the shell
856         final File dir = file.getParentFile();
857         dir.mkdirs();
858         if (!dir.exists()) {
859             throw new FileNotFoundException("Failed to create parent for " + file);
860         }
861         try (InputStream source = mContext.getAssets().open(resId);
862              OutputStream target = new FileOutputStream(file)) {
863             FileUtils.copy(source, target);
864         }
865         return file;
866     }
867 }
868