1 /*
2  * Copyright (C) 2019 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.content.Intent.FLAG_ACTIVITY_NEW_TASK;
19 
20 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
21 
22 import static org.junit.Assert.assertEquals;
23 import static org.junit.Assert.assertTrue;
24 import static org.junit.Assert.fail;
25 
26 import android.app.DownloadManager;
27 import android.content.BroadcastReceiver;
28 import android.content.ContentUris;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.pm.PackageManager;
32 import android.database.Cursor;
33 import android.net.Uri;
34 import android.os.Bundle;
35 import android.os.FileUtils;
36 import android.os.ParcelFileDescriptor;
37 import android.os.Process;
38 import android.os.RemoteCallback;
39 import android.os.SystemClock;
40 import android.provider.MediaStore;
41 import android.text.TextUtils;
42 import android.text.format.DateUtils;
43 import android.util.Log;
44 import android.webkit.cts.CtsTestServer;
45 
46 import androidx.test.InstrumentationRegistry;
47 
48 import com.android.compatibility.common.util.PollingCheck;
49 
50 import org.junit.After;
51 import org.junit.Before;
52 
53 import java.io.BufferedReader;
54 import java.io.File;
55 import java.io.FileInputStream;
56 import java.io.FileNotFoundException;
57 import java.io.FileOutputStream;
58 import java.io.InputStream;
59 import java.io.InputStreamReader;
60 import java.io.OutputStream;
61 import java.io.PrintWriter;
62 import java.nio.charset.StandardCharsets;
63 import java.security.DigestInputStream;
64 import java.security.MessageDigest;
65 import java.util.Arrays;
66 import java.util.HashSet;
67 import java.util.concurrent.CompletableFuture;
68 import java.util.concurrent.TimeUnit;
69 
70 public class DownloadManagerTestBase {
71     protected static final String TAG = "DownloadManagerTest";
72 
73     /**
74      * According to the CDD Section 7.6.1, the DownloadManager implementation must be able to
75      * download individual files of 100 MB.
76      */
77     protected static final int MINIMUM_DOWNLOAD_BYTES = 100 * 1024 * 1024;
78 
79     protected static final long SHORT_TIMEOUT = 5 * DateUtils.SECOND_IN_MILLIS;
80     protected static final long LONG_TIMEOUT = 3 * DateUtils.MINUTE_IN_MILLIS;
81     private static final String ACTION_CREATE_FILE_WITH_CONTENT =
82             "com.android.cts.action.CREATE_FILE_WITH_CONTENT";
83     private static final String EXTRA_PATH = "path";
84     private static final String EXTRA_CONTENTS = "contents";
85     private static final String EXTRA_CALLBACK = "callback";
86     private static final String KEY_ERROR = "error";
87     private static final String STORAGE_DELEGATOR_PACKAGE = "com.android.test.storagedelegator";
88 
89     protected Context mContext;
90     protected DownloadManager mDownloadManager;
91 
92     private CtsTestServer mWebServer;
93 
94     @Before
setUp()95     public void setUp() throws Exception {
96         mContext = InstrumentationRegistry.getTargetContext();
97         mDownloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE);
98         mWebServer = new CtsTestServer(mContext);
99         clearDownloads();
100     }
101 
102     @After
tearDown()103     public void tearDown() throws Exception {
104         mWebServer.shutdown();
105         clearDownloads();
106     }
107 
updateUri(Uri uri, String column, String value)108     protected void updateUri(Uri uri, String column, String value) throws Exception {
109         final String cmd = String.format("content update --uri %s --bind %s:s:%s",
110                 uri, column, value);
111         final String res = runShellCommand(cmd).trim();
112         assertTrue(res, TextUtils.isEmpty(res));
113     }
114 
hash(InputStream in)115     protected static byte[] hash(InputStream in) throws Exception {
116         try (DigestInputStream digestIn = new DigestInputStream(in,
117                 MessageDigest.getInstance("SHA-1"));
118              OutputStream out = new FileOutputStream(new File("/dev/null"))) {
119             FileUtils.copy(digestIn, out);
120             return digestIn.getMessageDigest().digest();
121         } finally {
122             FileUtils.closeQuietly(in);
123         }
124     }
125 
getMediaStoreUri(Uri downloadUri)126     protected static Uri getMediaStoreUri(Uri downloadUri) throws Exception {
127         final Context context = InstrumentationRegistry.getTargetContext();
128         Cursor cursor = context.getContentResolver().query(downloadUri, null, null, null);
129         if (cursor != null && cursor.moveToFirst()) {
130             // DownloadManager.COLUMN_MEDIASTORE_URI is not a column in the query result.
131             // COLUMN_MEDIAPROVIDER_URI value maybe the same as COLUMN_MEDIASTORE_URI but NOT
132             // guaranteed.
133             int index = cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIAPROVIDER_URI);
134             return Uri.parse(cursor.getString(index));
135         } else {
136             throw new FileNotFoundException("Failed to find entry for " + downloadUri);
137         }
138     }
139 
getMediaStoreColumnValue(Uri mediaStoreUri, String columnName)140     protected String getMediaStoreColumnValue(Uri mediaStoreUri, String columnName)
141             throws Exception {
142         if (!MediaStore.Files.FileColumns.MEDIA_TYPE.equals(columnName)) {
143             final int mediaType = getMediaType(mediaStoreUri);
144             final String volumeName = MediaStore.getVolumeName(mediaStoreUri);
145             final long id = ContentUris.parseId(mediaStoreUri);
146             switch (mediaType) {
147                 case MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO:
148                     mediaStoreUri = ContentUris.withAppendedId(
149                             MediaStore.Audio.Media.getContentUri(volumeName), id);
150                     break;
151                 case MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE:
152                     mediaStoreUri = ContentUris.withAppendedId(
153                             MediaStore.Images.Media.getContentUri(volumeName), id);
154                     break;
155                 case MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO:
156                     mediaStoreUri = ContentUris.withAppendedId(
157                             MediaStore.Video.Media.getContentUri(volumeName), id);
158                     break;
159             }
160         }
161         // Need to pass in the user id to support multi-user scenarios.
162         final int userId = getUserId();
163         final String cmd = String.format("content query --uri %s --projection %s --user %s",
164                 mediaStoreUri, columnName, userId);
165         final String res = runShellCommand(cmd).trim();
166         final String str = columnName + "=";
167         final int i = res.indexOf(str);
168         if (i >= 0) {
169             return res.substring(i + str.length());
170         } else {
171             throw new FileNotFoundException("Failed to find "
172                     + columnName + " for "
173                     + mediaStoreUri + "; found " + res);
174         }
175     }
176 
getMediaType(Uri mediaStoreUri)177     private int getMediaType(Uri mediaStoreUri) throws Exception {
178         final Uri filesUri = MediaStore.Files.getContentUri(
179                 MediaStore.getVolumeName(mediaStoreUri),
180                 ContentUris.parseId(mediaStoreUri));
181         return Integer.parseInt(getMediaStoreColumnValue(filesUri,
182                 MediaStore.Files.FileColumns.MEDIA_TYPE));
183     }
184 
getTotalBytes(InputStream in)185     protected int getTotalBytes(InputStream in) throws Exception {
186         try {
187             int total = 0;
188             final byte[] buf = new byte[4096];
189             int bytesRead;
190             while ((bytesRead = in.read(buf)) != -1) {
191                 total += bytesRead;
192             }
193             return total;
194         } finally {
195             FileUtils.closeQuietly(in);
196         }
197     }
198 
getUserId()199     private static int getUserId() {
200         return Process.myUserHandle().getIdentifier();
201     }
202 
getRawFilePath(Uri uri)203     protected static String getRawFilePath(Uri uri) throws Exception {
204         return getFileData(uri, "_data");
205     }
206 
getFileData(Uri uri, String projection)207     private static String getFileData(Uri uri, String projection) throws Exception {
208         final Context context = InstrumentationRegistry.getTargetContext();
209         final String[] projections =  new String[] { projection };
210         Cursor c = context.getContentResolver().query(uri, projections, null, null, null);
211         if (c != null && c.getCount() > 0) {
212             c.moveToFirst();
213             return c.getString(0);
214         } else {
215             String msg = String.format("Failed to find %s for %s", projection, uri);
216             throw new FileNotFoundException(msg);
217         }
218     }
219 
readContentsFromUri(Uri uri)220     protected static String readContentsFromUri(Uri uri) throws Exception {
221         final Context context = InstrumentationRegistry.getTargetContext();
222         try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) {
223             return readFromInputStream(inputStream);
224         }
225     }
226 
readFromRawFile(String filePath)227     protected static String readFromRawFile(String filePath) throws Exception {
228         Log.d(TAG, "Reading form file: " + filePath);
229         return readFromFile(
230             ParcelFileDescriptor.open(new File(filePath), ParcelFileDescriptor.MODE_READ_ONLY));
231     }
232 
readFromFile(ParcelFileDescriptor pfd)233     protected static String readFromFile(ParcelFileDescriptor pfd) throws Exception {
234         BufferedReader br = null;
235         try (final InputStream in = new FileInputStream(pfd.getFileDescriptor())) {
236             br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
237             String str;
238             StringBuilder out = new StringBuilder();
239             while ((str = br.readLine()) != null) {
240                 out.append(str);
241             }
242             return out.toString();
243         } finally {
244             if (br != null) {
245                 br.close();
246             }
247         }
248     }
249 
createFile(File baseDir, String fileName)250     protected static File createFile(File baseDir, String fileName) {
251         if (!baseDir.exists()) {
252             baseDir.mkdirs();
253         }
254         return new File(baseDir, fileName);
255     }
256 
deleteFromShell(File file)257     protected static void deleteFromShell(File file) {
258         runShellCommand("rm " + file);
259     }
260 
writeToFile(File file, String contents)261     protected static void writeToFile(File file, String contents) throws Exception {
262         file.getParentFile().mkdirs();
263         file.delete();
264 
265         try (final PrintWriter out = new PrintWriter(file)) {
266             out.print(contents);
267         }
268 
269         final String actual;
270         try (FileInputStream fis = new FileInputStream(file)) {
271             actual = readFromInputStream(fis);
272         }
273         assertEquals(contents, actual);
274     }
275 
writeToFileWithDelegator(File file, String contents)276     protected void writeToFileWithDelegator(File file, String contents) throws Exception {
277         final CompletableFuture<Bundle> callbackResult = new CompletableFuture<>();
278 
279         mContext.startActivity(new Intent(ACTION_CREATE_FILE_WITH_CONTENT)
280                 .setPackage(STORAGE_DELEGATOR_PACKAGE)
281                 .putExtra(EXTRA_PATH, file.getAbsolutePath())
282                 .putExtra(EXTRA_CONTENTS, contents)
283                 .setFlags(FLAG_ACTIVITY_NEW_TASK)
284                 .putExtra(EXTRA_CALLBACK, new RemoteCallback(callbackResult::complete)));
285 
286         final Bundle resultBundle = callbackResult.get(SHORT_TIMEOUT, TimeUnit.MILLISECONDS);
287         if (resultBundle.getString(KEY_ERROR) != null) {
288             fail("Failed to create the file " + file + ", error:" + resultBundle.getString(KEY_ERROR));
289         }
290     }
291 
readFromInputStream(InputStream inputStream)292     private static String readFromInputStream(InputStream inputStream) throws Exception {
293         final StringBuffer res = new StringBuffer();
294         final byte[] buf = new byte[512];
295         int bytesRead;
296         while ((bytesRead = inputStream.read(buf)) != -1) {
297             res.append(new String(buf, 0, bytesRead));
298         }
299         return res.toString();
300     }
301 
clearDownloads()302     protected void clearDownloads() {
303         if (getTotalNumberDownloads() > 0) {
304             Cursor cursor = null;
305             try {
306                 DownloadManager.Query query = new DownloadManager.Query();
307                 cursor = mDownloadManager.query(query);
308                 int columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_ID);
309                 long[] removeIds = new long[cursor.getCount()];
310                 for (int i = 0; cursor.moveToNext(); i++) {
311                     removeIds[i] = cursor.getLong(columnIndex);
312                 }
313                 assertEquals(removeIds.length, mDownloadManager.remove(removeIds));
314                 assertEquals(0, getTotalNumberDownloads());
315             } finally {
316                 if (cursor != null) {
317                     cursor.close();
318                 }
319             }
320         }
321     }
322 
getGoodUrl()323     protected Uri getGoodUrl() {
324         return Uri.parse(mWebServer.getTestDownloadUrl("cts-good-download", 0));
325     }
326 
getBadUrl()327     protected Uri getBadUrl() {
328         return Uri.parse(mWebServer.getBaseUri() + "/nosuchurl");
329     }
330 
getMinimumDownloadUrl()331     protected Uri getMinimumDownloadUrl() {
332         return Uri.parse(mWebServer.getTestDownloadUrl("cts-minimum-download",
333                 MINIMUM_DOWNLOAD_BYTES));
334     }
335 
getAssetUrl(String asset)336     protected Uri getAssetUrl(String asset) {
337         return Uri.parse(mWebServer.getAssetUrl(asset));
338     }
339 
getTotalNumberDownloads()340     protected int getTotalNumberDownloads() {
341         Cursor cursor = null;
342         try {
343             DownloadManager.Query query = new DownloadManager.Query();
344             cursor = mDownloadManager.query(query);
345             return cursor.getCount();
346         } finally {
347             if (cursor != null) {
348                 cursor.close();
349             }
350         }
351     }
352 
assertDownloadQueryableById(long downloadId)353     protected void assertDownloadQueryableById(long downloadId) {
354         Cursor cursor = null;
355         try {
356             DownloadManager.Query query = new DownloadManager.Query().setFilterById(downloadId);
357             cursor = mDownloadManager.query(query);
358             assertEquals(1, cursor.getCount());
359         } finally {
360             if (cursor != null) {
361                 cursor.close();
362             }
363         }
364     }
365 
assertDownloadQueryableByStatus(final int status)366     protected void assertDownloadQueryableByStatus(final int status) {
367         new PollingCheck() {
368             @Override
369             protected boolean check() {
370                 Cursor cursor= null;
371                 try {
372                     DownloadManager.Query query = new DownloadManager.Query().setFilterByStatus(status);
373                     cursor = mDownloadManager.query(query);
374                     return 1 == cursor.getCount();
375                 } finally {
376                     if (cursor != null) {
377                         cursor.close();
378                     }
379                 }
380             }
381         }.run();
382     }
383 
assertSuccessfulDownload(long id, File location)384     protected void assertSuccessfulDownload(long id, File location) throws Exception {
385         Cursor cursor = null;
386         try {
387             final File expectedLocation = location.getCanonicalFile();
388             cursor = mDownloadManager.query(new DownloadManager.Query().setFilterById(id));
389             assertTrue(cursor.moveToNext());
390             assertEquals(DownloadManager.STATUS_SUCCESSFUL, cursor.getInt(
391                     cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)));
392             assertEquals(Uri.fromFile(expectedLocation).toString(),
393                     cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)));
394             assertTrue(expectedLocation.exists());
395         } finally {
396             if (cursor != null) {
397                 cursor.close();
398             }
399         }
400     }
401 
assertRemoveDownload(long removeId, int expectedNumDownloads)402     protected void assertRemoveDownload(long removeId, int expectedNumDownloads) {
403         Cursor cursor = null;
404         try {
405             assertEquals(1, mDownloadManager.remove(removeId));
406             DownloadManager.Query query = new DownloadManager.Query();
407             cursor = mDownloadManager.query(query);
408             assertEquals(expectedNumDownloads, cursor.getCount());
409         } finally {
410             if (cursor != null) {
411                 cursor.close();
412             }
413         }
414     }
415 
hasInternetConnection()416     protected boolean hasInternetConnection() {
417         final PackageManager pm = mContext.getPackageManager();
418         return pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)
419                 || pm.hasSystemFeature(PackageManager.FEATURE_WIFI)
420                 || pm.hasSystemFeature(PackageManager.FEATURE_ETHERNET);
421     }
422 
423     public static class DownloadCompleteReceiver extends BroadcastReceiver {
424         private HashSet<Long> mCompleteIds = new HashSet<>();
425 
426         @Override
onReceive(Context context, Intent intent)427         public void onReceive(Context context, Intent intent) {
428             synchronized (mCompleteIds) {
429                 mCompleteIds.add(intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1));
430                 mCompleteIds.notifyAll();
431             }
432         }
433 
isCompleteLocked(long... ids)434         private boolean isCompleteLocked(long... ids) {
435             for (long id : ids) {
436                 if (!mCompleteIds.contains(id)) {
437                     return false;
438                 }
439             }
440             return true;
441         }
442 
waitForDownloadComplete(long timeoutMillis, long... waitForIds)443         public void waitForDownloadComplete(long timeoutMillis, long... waitForIds)
444                 throws InterruptedException {
445             if (waitForIds.length == 0) {
446                 throw new IllegalArgumentException("Missing IDs to wait for");
447             }
448 
449             final long startTime = SystemClock.elapsedRealtime();
450             do {
451                 synchronized (mCompleteIds) {
452                     mCompleteIds.wait(timeoutMillis);
453                     if (isCompleteLocked(waitForIds)) return;
454                 }
455             } while ((SystemClock.elapsedRealtime() - startTime) < timeoutMillis);
456 
457             throw new InterruptedException("Timeout waiting for IDs " + Arrays.toString(waitForIds)
458                     + "; received " + mCompleteIds.toString()
459                     + ".  Make sure you have WiFi or some other connectivity for this test.");
460         }
461     }
462 }
463