1 /* 2 * Copyright (C) 2010 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 17 package android.os.storage; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.content.res.Resources.NotFoundException; 22 import android.os.Environment; 23 import android.os.SystemClock; 24 import android.test.InstrumentationTestCase; 25 import android.util.Log; 26 import android.os.Environment; 27 import android.os.FileUtils; 28 import android.os.storage.OnObbStateChangeListener; 29 import android.os.storage.StorageManager; 30 31 import java.io.BufferedReader; 32 import java.io.DataInputStream; 33 import java.io.File; 34 import java.io.FileInputStream; 35 import java.io.FileNotFoundException; 36 import java.io.FileReader; 37 import java.io.InputStream; 38 import java.io.IOException; 39 import java.io.StringReader; 40 41 public class StorageManagerBaseTest extends InstrumentationTestCase { 42 43 protected Context mContext = null; 44 protected StorageManager mSm = null; 45 private static String LOG_TAG = "StorageManagerBaseTest"; 46 protected static final long MAX_WAIT_TIME = 120*1000; 47 protected static final long WAIT_TIME_INCR = 5*1000; 48 protected static String OBB_FILE_1 = "obb_file1.obb"; 49 protected static String OBB_FILE_1_CONTENTS_1 = "OneToOneThousandInts.bin"; 50 protected static String OBB_FILE_2 = "obb_file2.obb"; 51 protected static String OBB_FILE_3 = "obb_file3.obb"; 52 protected static String OBB_FILE_1_PASSWORD = "password1"; 53 protected static String OBB_FILE_1_ENCRYPTED = "obb_enc_file100_orig1.obb"; 54 protected static String OBB_FILE_2_UNSIGNED = "obb_file2_nosign.obb"; 55 protected static String OBB_FILE_3_PASSWORD = "password3"; 56 protected static String OBB_FILE_3_ENCRYPTED = "obb_enc_file100_orig3.obb"; 57 protected static String OBB_FILE_3_BAD_PACKAGENAME = "obb_file3_bad_packagename.obb"; 58 59 protected static boolean FORCE = true; 60 protected static boolean DONT_FORCE = false; 61 62 private static final String SAMPLE1_TEXT = "This is sample text.\n\nTesting 1 2 3."; 63 64 private static final String SAMPLE2_TEXT = 65 "We the people of the United States, in order to form a more perfect union,\n" 66 + "establish justice, insure domestic tranquility, provide for the common\n" 67 + "defense, promote the general welfare, and secure the blessings of liberty\n" 68 + "to ourselves and our posterity, do ordain and establish this Constitution\n" 69 + "for the United States of America.\n\n"; 70 71 class MountingObbThread extends Thread { 72 boolean mStop = false; 73 volatile boolean mFileOpenOnObb = false; 74 private String mObbFilePath = null; 75 private String mPathToContentsFile = null; 76 private String mOfficialObbFilePath = null; 77 78 /** 79 * Constructor 80 * 81 * @param obbFilePath path to the OBB image file 82 * @param pathToContentsFile path to a file on the mounted OBB volume to open after the OBB 83 * has been mounted 84 */ 85 public MountingObbThread (String obbFilePath, String pathToContentsFile) { 86 assertTrue("obbFilePath cannot be null!", obbFilePath != null); 87 mObbFilePath = obbFilePath; 88 assertTrue("path to contents file cannot be null!", pathToContentsFile != null); 89 mPathToContentsFile = pathToContentsFile; 90 } 91 92 /** 93 * Runs the thread 94 * 95 * Mounts OBB_FILE_1, and tries to open a file on the mounted OBB (specified in the 96 * constructor). Once it's open, it waits until someone calls its doStop(), after which it 97 * closes the opened file. 98 */ 99 public void run() { 100 // the official OBB file path and the mount-request file path should be the same, but 101 // let's distinguish the two as they may make for some interesting tests later 102 mOfficialObbFilePath = mountObb(mObbFilePath); 103 assertEquals("Expected and actual OBB file paths differ!", mObbFilePath, 104 mOfficialObbFilePath); 105 106 // open a file on OBB 1... 107 DataInputStream inputFile = openFileOnMountedObb(mOfficialObbFilePath, 108 mPathToContentsFile); 109 assertTrue("Failed to open file!", inputFile != null); 110 111 synchronized (this) { 112 mFileOpenOnObb = true; 113 notifyAll(); 114 } 115 116 while (!mStop) { 117 try { 118 Thread.sleep(WAIT_TIME_INCR); 119 } catch (InterruptedException e) { 120 // nothing special to be done for interruptions 121 } 122 } 123 try { 124 inputFile.close(); 125 } catch (IOException e) { 126 fail("Failed to close file on OBB due to error: " + e.toString()); 127 } 128 } 129 130 /** 131 * Tells whether a file has yet been successfully opened on the OBB or not 132 * 133 * @return true if the specified file on the OBB was opened; false otherwise 134 */ 135 public boolean isFileOpenOnObb() { 136 return mFileOpenOnObb; 137 } 138 139 /** 140 * Returns the official path of the OBB file that was mounted 141 * 142 * This is not the mount path, but the normalized path to the actual OBB file 143 * 144 * @return a {@link String} representation of the path to the OBB file that was mounted 145 */ 146 public String officialObbFilePath() { 147 return mOfficialObbFilePath; 148 } 149 150 /** 151 * Requests the thread to stop running 152 * 153 * Closes the opened file and returns 154 */ 155 public void doStop() { 156 mStop = true; 157 } 158 } 159 160 public class ObbListener extends OnObbStateChangeListener { 161 private String LOG_TAG = "StorageManagerBaseTest.ObbListener"; 162 163 String mOfficialPath = null; 164 boolean mDone = false; 165 int mState = -1; 166 167 /** 168 * {@inheritDoc} 169 */ 170 @Override 171 public void onObbStateChange(String path, int state) { 172 Log.i(LOG_TAG, "Storage state changing to: " + state); 173 174 synchronized (this) { 175 Log.i(LOG_TAG, "OfficialPath is now: " + path); 176 mState = state; 177 mOfficialPath = path; 178 mDone = true; 179 notifyAll(); 180 } 181 } 182 183 /** 184 * Tells whether we are done or not (system told us the OBB has changed state) 185 * 186 * @return true if the system has told us this OBB's state has changed, false otherwise 187 */ 188 public boolean isDone() { 189 return mDone; 190 } 191 192 /** 193 * The last state of the OBB, according to the system 194 * 195 * @return A {@link String} representation of the state of the OBB 196 */ 197 public int state() { 198 return mState; 199 } 200 201 /** 202 * The normalized, official path to the OBB file (according to the system) 203 * 204 * @return A {@link String} representation of the official path to the OBB file 205 */ 206 public String officialPath() { 207 return mOfficialPath; 208 } 209 } 210 211 /** 212 * {@inheritDoc} 213 */ 214 @Override 215 public void setUp() throws Exception { 216 mContext = getInstrumentation().getContext(); 217 mSm = (StorageManager)mContext.getSystemService(android.content.Context.STORAGE_SERVICE); 218 219 } 220 221 /** 222 * Helper to copy a raw resource file to an actual specified file 223 * 224 * @param rawResId The raw resource ID of the OBB resource file 225 * @param outFile A File representing the file we want to copy the OBB to 226 * @throws NotFoundException If the resource file could not be found 227 */ 228 private void copyRawToFile(int rawResId, File outFile) throws NotFoundException { 229 Resources res = mContext.getResources(); 230 InputStream is = null; 231 try { 232 is = res.openRawResource(rawResId); 233 } catch (NotFoundException e) { 234 Log.i(LOG_TAG, "Failed to load resource with id: " + rawResId); 235 throw e; 236 } 237 FileUtils.setPermissions(outFile.getPath(), FileUtils.S_IRWXU | FileUtils.S_IRWXG 238 | FileUtils.S_IRWXO, -1, -1); 239 assertTrue(FileUtils.copyToFile(is, outFile)); 240 FileUtils.setPermissions(outFile.getPath(), FileUtils.S_IRWXU | FileUtils.S_IRWXG 241 | FileUtils.S_IRWXO, -1, -1); 242 } 243 244 /** 245 * Creates an OBB file (with the given name), into the app's standard files directory 246 * 247 * @param name The name of the OBB file we want to create/write to 248 * @param rawResId The raw resource ID of the OBB file in the package 249 * @return A {@link File} representing the file to write to 250 */ 251 protected File createObbFile(String name, int rawResId) { 252 File outFile = null; 253 try { 254 final File filesDir = mContext.getFilesDir(); 255 outFile = new File(filesDir, name); 256 copyRawToFile(rawResId, outFile); 257 } catch (NotFoundException e) { 258 if (outFile != null) { 259 outFile.delete(); 260 } 261 } 262 return outFile; 263 } 264 265 /** 266 * Mounts an OBB file and opens a file located on it 267 * 268 * @param obbPath Path to OBB image 269 * @param fileName The full name and path to the file on the OBB to open once the OBB is mounted 270 * @return The {@link DataInputStream} representing the opened file, if successful in opening 271 * the file, or null of unsuccessful. 272 */ 273 protected DataInputStream openFileOnMountedObb(String obbPath, String fileName) { 274 275 // get mSm obb mount path 276 assertTrue("Cannot open file when OBB is not mounted!", mSm.isObbMounted(obbPath)); 277 278 String path = mSm.getMountedObbPath(obbPath); 279 assertTrue("Path should not be null!", path != null); 280 281 File inFile = new File(path, fileName); 282 DataInputStream inStream = null; 283 try { 284 inStream = new DataInputStream(new FileInputStream(inFile)); 285 Log.i(LOG_TAG, "Opened file: " + fileName + " for read at path: " + path); 286 } catch (FileNotFoundException e) { 287 Log.e(LOG_TAG, e.toString()); 288 return null; 289 } catch (SecurityException e) { 290 Log.e(LOG_TAG, e.toString()); 291 return null; 292 } 293 return inStream; 294 } 295 296 /** 297 * Mounts an OBB file 298 * 299 * @param obbFilePath The full path to the OBB file to mount 300 * @param key (optional) The key to use to unencrypt the OBB; pass null for no encryption 301 * @param expectedState The expected state resulting from trying to mount the OBB 302 * @return A {@link String} representing the normalized path to OBB file that was mounted 303 */ 304 protected String mountObb(String obbFilePath, String key, int expectedState) { 305 return doMountObb(obbFilePath, key, expectedState); 306 } 307 308 /** 309 * Mounts an OBB file with default options (no encryption, mounting succeeds) 310 * 311 * @param obbFilePath The full path to the OBB file to mount 312 * @return A {@link String} representing the normalized path to OBB file that was mounted 313 */ 314 protected String mountObb(String obbFilePath) { 315 return doMountObb(obbFilePath, null, OnObbStateChangeListener.MOUNTED); 316 } 317 318 /** 319 * Synchronously waits for an OBB listener to be signaled of a state change, but does not throw 320 * 321 * @param obbListener The listener for the OBB file 322 * @return true if the listener was signaled of a state change by the system, else returns 323 * false if we time out. 324 */ 325 protected boolean doWaitForObbStateChange(ObbListener obbListener) { 326 synchronized(obbListener) { 327 long waitTimeMillis = 0; 328 while (!obbListener.isDone()) { 329 try { 330 Log.i(LOG_TAG, "Waiting for listener..."); 331 obbListener.wait(WAIT_TIME_INCR); 332 Log.i(LOG_TAG, "Awoke from waiting for listener..."); 333 waitTimeMillis += WAIT_TIME_INCR; 334 if (waitTimeMillis > MAX_WAIT_TIME) { 335 fail("Timed out waiting for OBB state to change!"); 336 } 337 } catch (InterruptedException e) { 338 Log.i(LOG_TAG, e.toString()); 339 } 340 } 341 return obbListener.isDone(); 342 } 343 } 344 345 /** 346 * Synchronously waits for an OBB listener to be signaled of a state change 347 * 348 * @param obbListener The listener for the OBB file 349 * @return true if the listener was signaled of a state change by the system; else a fail() 350 * is triggered if we timed out 351 */ 352 protected String doMountObb_noThrow(String obbFilePath, String key, int expectedState) { 353 Log.i(LOG_TAG, "doMountObb() on " + obbFilePath + " using key: " + key); 354 assertTrue ("Null path was passed in for OBB file!", obbFilePath != null); 355 assertTrue ("Null path was passed in for OBB file!", obbFilePath != null); 356 357 ObbListener obbListener = new ObbListener(); 358 boolean success = mSm.mountObb(obbFilePath, key, obbListener); 359 success &= obbFilePath.equals(doWaitForObbStateChange(obbListener)); 360 success &= (expectedState == obbListener.state()); 361 362 if (OnObbStateChangeListener.MOUNTED == expectedState) { 363 success &= obbFilePath.equals(obbListener.officialPath()); 364 success &= mSm.isObbMounted(obbListener.officialPath()); 365 } else { 366 success &= !mSm.isObbMounted(obbListener.officialPath()); 367 } 368 369 if (success) { 370 return obbListener.officialPath(); 371 } else { 372 return null; 373 } 374 } 375 376 /** 377 * Mounts an OBB file without throwing and synchronously waits for it to finish mounting 378 * 379 * @param obbFilePath The full path to the OBB file to mount 380 * @param key (optional) The key to use to unencrypt the OBB; pass null for no encryption 381 * @param expectedState The expected state resulting from trying to mount the OBB 382 * @return A {@link String} representing the actual normalized path to OBB file that was 383 * mounted, or null if the mounting failed 384 */ 385 protected String doMountObb(String obbFilePath, String key, int expectedState) { 386 Log.i(LOG_TAG, "doMountObb() on " + obbFilePath + " using key: " + key); 387 assertTrue ("Null path was passed in for OBB file!", obbFilePath != null); 388 389 ObbListener obbListener = new ObbListener(); 390 assertTrue("mountObb call failed", mSm.mountObb(obbFilePath, key, obbListener)); 391 assertTrue("Failed to get OBB mount status change for file: " + obbFilePath, 392 doWaitForObbStateChange(obbListener)); 393 assertEquals("OBB mount state not what was expected!", expectedState, obbListener.state()); 394 395 if (OnObbStateChangeListener.MOUNTED == expectedState) { 396 assertEquals(obbFilePath, obbListener.officialPath()); 397 assertTrue("Obb should be mounted, but SM reports it is not!", 398 mSm.isObbMounted(obbListener.officialPath())); 399 } else if (OnObbStateChangeListener.UNMOUNTED == expectedState) { 400 assertFalse("Obb should not be mounted, but SM reports it is!", 401 mSm.isObbMounted(obbListener.officialPath())); 402 } 403 404 assertEquals("Mount state is not what was expected!", expectedState, obbListener.state()); 405 return obbListener.officialPath(); 406 } 407 408 /** 409 * Unmounts an OBB file without throwing, and synchronously waits for it to finish unmounting 410 * 411 * @param obbFilePath The full path to the OBB file to mount 412 * @param force true if we shuold force the unmount, false otherwise 413 * @return true if the unmount was successful, false otherwise 414 */ 415 protected boolean unmountObb_noThrow(String obbFilePath, boolean force) { 416 Log.i(LOG_TAG, "doUnmountObb_noThrow() on " + obbFilePath); 417 assertTrue ("Null path was passed in for OBB file!", obbFilePath != null); 418 boolean success = true; 419 420 ObbListener obbListener = new ObbListener(); 421 assertTrue("unmountObb call failed", mSm.unmountObb(obbFilePath, force, obbListener)); 422 423 boolean stateChanged = doWaitForObbStateChange(obbListener); 424 if (force) { 425 success &= stateChanged; 426 success &= (OnObbStateChangeListener.UNMOUNTED == obbListener.state()); 427 success &= !mSm.isObbMounted(obbFilePath); 428 } 429 return success; 430 } 431 432 /** 433 * Unmounts an OBB file and synchronously waits for it to finish unmounting 434 * 435 * @param obbFilePath The full path to the OBB file to mount 436 * @param force true if we shuold force the unmount, false otherwise 437 */ 438 protected void unmountObb(String obbFilePath, boolean force) { 439 Log.i(LOG_TAG, "doUnmountObb() on " + obbFilePath); 440 assertTrue ("Null path was passed in for OBB file!", obbFilePath != null); 441 442 ObbListener obbListener = new ObbListener(); 443 assertTrue("unmountObb call failed", mSm.unmountObb(obbFilePath, force, obbListener)); 444 445 boolean stateChanged = doWaitForObbStateChange(obbListener); 446 if (force) { 447 assertTrue("Timed out waiting to unmount OBB file " + obbFilePath, stateChanged); 448 assertEquals("OBB failed to unmount", OnObbStateChangeListener.UNMOUNTED, 449 obbListener.state()); 450 assertFalse("Obb should NOT be mounted, but SM reports it is!", mSm.isObbMounted( 451 obbFilePath)); 452 } 453 } 454 455 /** 456 * Helper to validate the contents of an "int" file in an OBB. 457 * 458 * The format of the files are sequential int's, in the range of: [start..end) 459 * 460 * @param path The full path to the file (path to OBB) 461 * @param filename The filename containing the ints to validate 462 * @param start The first int expected to be found in the file 463 * @param end The last int + 1 expected to be found in the file 464 */ 465 protected void doValidateIntContents(String path, String filename, int start, int end) { 466 File inFile = new File(path, filename); 467 DataInputStream inStream = null; 468 Log.i(LOG_TAG, "Validating file " + filename + " at " + path); 469 try { 470 inStream = new DataInputStream(new FileInputStream(inFile)); 471 472 for (int i = start; i < end; ++i) { 473 if (inStream.readInt() != i) { 474 fail("Unexpected value read in OBB file"); 475 } 476 } 477 if (inStream != null) { 478 inStream.close(); 479 } 480 Log.i(LOG_TAG, "Successfully validated file " + filename); 481 } catch (FileNotFoundException e) { 482 fail("File " + inFile + " not found: " + e.toString()); 483 } catch (IOException e) { 484 fail("IOError with file " + inFile + ":" + e.toString()); 485 } 486 } 487 488 /** 489 * Helper to validate the contents of a text file in an OBB 490 * 491 * @param path The full path to the file (path to OBB) 492 * @param filename The filename containing the ints to validate 493 * @param contents A {@link String} containing the expected contents of the file 494 */ 495 protected void doValidateTextContents(String path, String filename, String contents) { 496 File inFile = new File(path, filename); 497 BufferedReader fileReader = null; 498 BufferedReader textReader = null; 499 Log.i(LOG_TAG, "Validating file " + filename + " at " + path); 500 try { 501 fileReader = new BufferedReader(new FileReader(inFile)); 502 textReader = new BufferedReader(new StringReader(contents)); 503 String actual = null; 504 String expected = null; 505 while ((actual = fileReader.readLine()) != null) { 506 expected = textReader.readLine(); 507 if (!actual.equals(expected)) { 508 fail("File " + filename + " in OBB " + path + " does not match expected value"); 509 } 510 } 511 fileReader.close(); 512 textReader.close(); 513 Log.i(LOG_TAG, "File " + filename + " successfully verified."); 514 } catch (IOException e) { 515 fail("IOError with file " + inFile + ":" + e.toString()); 516 } 517 } 518 519 /** 520 * Helper to validate the contents of a "long" file on our OBBs 521 * 522 * The format of the files are sequential 0's of type long 523 * 524 * @param path The full path to the file (path to OBB) 525 * @param filename The filename containing the ints to validate 526 * @param size The number of zero's expected in the file 527 * @param checkContents If true, the contents of the file are actually verified; if false, 528 * we simply verify that the file can be opened 529 */ 530 protected void doValidateZeroLongFile(String path, String filename, long size, 531 boolean checkContents) { 532 File inFile = new File(path, filename); 533 DataInputStream inStream = null; 534 Log.i(LOG_TAG, "Validating file " + filename + " at " + path); 535 try { 536 inStream = new DataInputStream(new FileInputStream(inFile)); 537 538 if (checkContents) { 539 for (long i = 0; i < size; ++i) { 540 if (inStream.readLong() != 0) { 541 fail("Unexpected value read in OBB file" + filename); 542 } 543 } 544 } 545 546 if (inStream != null) { 547 inStream.close(); 548 } 549 Log.i(LOG_TAG, "File " + filename + " successfully verified for " + size + " zeros"); 550 } catch (IOException e) { 551 fail("IOError with file " + inFile + ":" + e.toString()); 552 } 553 } 554 555 /** 556 * Helper to synchronously wait until we can get a path for a given OBB file 557 * 558 * @param filePath The full normalized path to the OBB file 559 * @return The mounted path of the OBB, used to access contents in it 560 */ 561 protected String doWaitForPath(String filePath) { 562 String path = null; 563 564 long waitTimeMillis = 0; 565 assertTrue("OBB " + filePath + " is not currently mounted!", mSm.isObbMounted(filePath)); 566 while (path == null) { 567 try { 568 Thread.sleep(WAIT_TIME_INCR); 569 waitTimeMillis += WAIT_TIME_INCR; 570 if (waitTimeMillis > MAX_WAIT_TIME) { 571 fail("Timed out waiting to get path of OBB file " + filePath); 572 } 573 } catch (InterruptedException e) { 574 // do nothing 575 } 576 path = mSm.getMountedObbPath(filePath); 577 } 578 Log.i(LOG_TAG, "Got OBB path: " + path); 579 return path; 580 } 581 582 /** 583 * Verifies the pre-defined contents of our first OBB (OBB_FILE_1) 584 * 585 * The OBB contains 4 files and no subdirectories 586 * 587 * @param filePath The normalized path to the already-mounted OBB file 588 */ 589 protected void verifyObb1Contents(String filePath) { 590 String path = null; 591 path = doWaitForPath(filePath); 592 593 // Validate contents of 2 files in this obb 594 doValidateIntContents(path, "OneToOneThousandInts.bin", 0, 1000); 595 doValidateIntContents(path, "SevenHundredInts.bin", 0, 700); 596 doValidateZeroLongFile(path, "FiveLongs.bin", 5, true); 597 } 598 599 /** 600 * Verifies the pre-defined contents of our second OBB (OBB_FILE_2) 601 * 602 * The OBB contains 2 files and no subdirectories 603 * 604 * @param filePath The normalized path to the already-mounted OBB file 605 */ 606 protected void verifyObb2Contents(String filename) { 607 String path = null; 608 path = doWaitForPath(filename); 609 610 // Validate contents of file 611 doValidateTextContents(path, "sample.txt", SAMPLE1_TEXT); 612 doValidateTextContents(path, "sample2.txt", SAMPLE2_TEXT); 613 } 614 615 /** 616 * Verifies the pre-defined contents of our third OBB (OBB_FILE_3) 617 * 618 * The OBB contains nested files and subdirectories 619 * 620 * @param filePath The normalized path to the already-mounted OBB file 621 */ 622 protected void verifyObb3Contents(String filename) { 623 String path = null; 624 path = doWaitForPath(filename); 625 626 // Validate contents of file 627 doValidateIntContents(path, "OneToOneThousandInts.bin", 0, 1000); 628 doValidateZeroLongFile(path, "TwoHundredLongs", 200, true); 629 630 // validate subdirectory 1 631 doValidateZeroLongFile(path + File.separator + "subdir1", "FiftyLongs", 50, true); 632 633 // validate subdirectory subdir2/ 634 doValidateIntContents(path + File.separator + "subdir2", "OneToOneThousandInts", 0, 1000); 635 636 // validate subdirectory subdir2/subdir2a/ 637 doValidateZeroLongFile(path + File.separator + "subdir2" + File.separator + "subdir2a", 638 "TwoHundredLongs", 200, true); 639 640 // validate subdirectory subdir2/subdir2a/subdir2a1/ 641 doValidateIntContents(path + File.separator + "subdir2" + File.separator + "subdir2a" 642 + File.separator + "subdir2a1", "OneToOneThousandInts", 0, 1000); 643 } 644 }