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