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