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