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