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
17package android.os.storage;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.content.res.Resources.NotFoundException;
22import android.os.Environment;
23import android.os.SystemClock;
24import android.test.InstrumentationTestCase;
25import android.util.Log;
26import android.os.Environment;
27import android.os.FileUtils;
28import android.os.storage.OnObbStateChangeListener;
29import android.os.storage.StorageManager;
30
31import java.io.BufferedReader;
32import java.io.DataInputStream;
33import java.io.File;
34import java.io.FileInputStream;
35import java.io.FileNotFoundException;
36import java.io.FileReader;
37import java.io.InputStream;
38import java.io.IOException;
39import java.io.StringReader;
40
41public 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}