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