1 /*
2  * Copyright (C) 2008 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.cts;
18 
19 import android.os.Environment;
20 import android.os.FileObserver;
21 import android.platform.test.annotations.AppModeFull;
22 import android.platform.test.annotations.AppModeInstant;
23 import android.test.AndroidTestCase;
24 import android.util.Pair;
25 
26 import androidx.test.InstrumentationRegistry;
27 
28 import java.io.File;
29 import java.io.FileOutputStream;
30 import java.util.ArrayList;
31 import java.util.Arrays;
32 import java.util.List;
33 
34 public class FileObserverTest extends AndroidTestCase {
35     private static final String PATH = "/PATH";
36     private static final String TEST_FILE = "file_observer_test.txt";
37     private static final String TEST_DIR = "fileobserver_dir";
38     private static final File EXT_STORAGE_DIR = new File(Environment.getExternalStorageDirectory(), "fileobserver_toplevel_dir");
39     private static final int FILE_DATA = 0x20;
40     private static final int UNDEFINED = 0x8000;
41     private static final long DELAY_MSECOND = 2000;
42 
helpSetUp(File dir)43     private void helpSetUp(File dir) throws Exception {
44         File testFile = new File(dir, TEST_FILE);
45         testFile.createNewFile();
46         File testDir = new File(dir, TEST_DIR);
47         testDir.mkdirs();
48     }
49 
50     @Override
setUp()51     protected void setUp() throws Exception {
52         super.setUp();
53         File dir = getContext().getFilesDir();
54         helpSetUp(dir);
55 
56         dir = getContext().getCacheDir();
57         helpSetUp(dir);
58 
59         // Instant apps cannot access external storage
60         if (!InstrumentationRegistry.getTargetContext().getPackageManager().isInstantApp()) {
61             dir = getContext().getExternalFilesDir(null);
62             helpSetUp(dir);
63 
64             dir = EXT_STORAGE_DIR;
65             dir.mkdirs();
66             helpSetUp(dir);
67         }
68 
69         // Let the setup settles
70         Thread.sleep(DELAY_MSECOND);
71     }
72 
helpTearDown(File dir)73     private void helpTearDown(File dir) throws Exception {
74         File testFile = new File(dir, TEST_FILE);
75         File testDir = new File(dir, TEST_DIR);
76         File moveDestFile = new File(testDir, TEST_FILE);
77 
78         if (testFile.exists()) {
79             testFile.delete();
80         }
81 
82         if (moveDestFile.exists()) {
83             moveDestFile.delete();
84         }
85 
86         if (testDir.exists()) {
87             testDir.delete();
88         }
89     }
90 
91     @Override
tearDown()92     protected void tearDown() throws Exception {
93         super.tearDown();
94 
95         File dir = getContext().getFilesDir();
96         helpTearDown(dir);
97 
98         dir = getContext().getCacheDir();
99         helpTearDown(dir);
100 
101         dir = getContext().getExternalFilesDir(null);
102         helpTearDown(dir);
103 
104         dir = EXT_STORAGE_DIR;
105         helpTearDown(dir);
106         if (dir.exists()) {
107             dir.delete();
108         }
109     }
110 
testConstructor()111     public void testConstructor() {
112         // new the instance
113         new MockFileObserver(new File(PATH));
114         // new the instance
115         new MockFileObserver(new File(PATH), FileObserver.ACCESS);
116     }
117 
118     /*
119      * Test point
120      * 1. Observe a dir, when it's child file have been written and closed,
121      * observer should get modify open-child modify-child and closed-write events.
122      * 2. While stop observer a dir, observer should't get any event while delete it's child file.
123      * 3. Observer a dir, when create delete a child file and delete self,
124      * observer should get create-child close-nowrite delete-child delete-self events.
125      * 4. Observer a file, the file moved from dir and the file moved to dir, move the file,
126      * file observer should get move-self event,
127      * moved from dir observer should get moved-from event,
128      * moved to dir observer should get moved-to event.
129      *
130      * On emulated storage, there may be additional operations related to case insensitivity, so
131      * we just check that the expected ones are present.
132      */
helpTestFileObserver(File dir, boolean isEmulated)133     public void helpTestFileObserver(File dir, boolean isEmulated) throws Exception {
134         MockFileObserver fileObserver = null;
135         int[] expected = null;
136         FileEvent[] moveEvents = null;
137         File testFile = new File(dir, TEST_FILE);
138         File testDir = new File(dir, TEST_DIR);
139         File moveDestFile;
140         FileOutputStream out = null;
141 
142         fileObserver = new MockFileObserver(testFile.getParentFile());
143         try {
144             fileObserver.startWatching();
145 
146             verifyTriggeredEventsOnFile(fileObserver, testFile, isEmulated);
147 
148             fileObserver.stopWatching();
149 
150             // action after observer stop watching
151             testFile.delete(); // delete
152 
153             // should not get any event
154             expected = new int[] {UNDEFINED};
155             moveEvents = waitForEvent(fileObserver);
156             if (isEmulated)
157                 assertEventsContains(testFile, expected, moveEvents);
158             else
159                 assertEventsEquals(testFile, expected, moveEvents);
160         } finally {
161             fileObserver.stopWatching();
162             if (out != null)
163                 out.close();
164             out = null;
165         }
166         fileObserver = new MockFileObserver(testDir);
167         try {
168             fileObserver.startWatching();
169             verifyTriggeredEventsOnDir(fileObserver, testDir, isEmulated);
170         } finally {
171             fileObserver.stopWatching();
172         }
173         dir = getContext().getFilesDir();
174         testFile = new File(dir, TEST_FILE);
175         testFile.createNewFile();
176         testDir = new File(dir, TEST_DIR);
177         testDir.mkdirs();
178         moveDestFile = new File(testDir, TEST_FILE);
179         final MockFileObserver movedFileObserver = new MockFileObserver(Arrays.asList(
180                 dir,
181                 testDir,
182                 testFile
183         ));
184         try {
185             movedFileObserver.startWatching();
186 
187             testFile.renameTo(moveDestFile);
188 
189             expected = new int[] {
190                     FileObserver.MOVED_FROM,
191                     FileObserver.MOVED_TO,
192                     FileObserver.MOVE_SELF,
193             };
194             moveEvents = waitForEvent(movedFileObserver);
195             if (isEmulated) {
196                 assertEventsContains(testFile, expected, moveEvents);
197             } else {
198                 assertEventsEquals(testFile, expected, moveEvents);
199             }
200         } finally {
201             movedFileObserver.stopWatching();
202         }
203 
204         // Because Javadoc didn't specify when should a event happened,
205         // here ACCESS ATTRIB we found no way to test.
206     }
207 
verifyTriggeredEventsOnFile(MockFileObserver fileObserver, File testFile, boolean isEmulated)208     private void verifyTriggeredEventsOnFile(MockFileObserver fileObserver,
209             File testFile, boolean isEmulated) throws Exception {
210         final FileOutputStream out = new FileOutputStream(testFile);
211 
212         out.write(FILE_DATA); // modify, open, write, modify
213         out.close(); // close_write
214 
215         final int[] expected = {
216                 FileObserver.MODIFY,
217                 FileObserver.OPEN,
218                 FileObserver.MODIFY,
219                 FileObserver.CLOSE_WRITE
220         };
221 
222         final FileEvent[] moveEvents = waitForEvent(fileObserver);
223         if (isEmulated) {
224             assertEventsContains(testFile, expected, moveEvents);
225         } else {
226             assertEventsEquals(testFile, expected, moveEvents);
227         }
228     }
229 
verifyTriggeredEventsOnDir(MockFileObserver fileObserver, File testDir, boolean isEmulated)230     private void verifyTriggeredEventsOnDir(MockFileObserver fileObserver,
231             File testDir, boolean isEmulated) throws Exception {
232         final File testFile = new File(testDir, TEST_FILE);
233         assertTrue(testFile.createNewFile());
234         assertTrue(testFile.exists());
235         testFile.delete();
236         testDir.delete();
237 
238         final int[] expected = {
239                 FileObserver.CREATE,
240                 FileObserver.OPEN,
241                 FileObserver.CLOSE_WRITE,
242                 FileObserver.DELETE,
243                 FileObserver.DELETE_SELF,
244                 UNDEFINED
245         };
246 
247         final FileEvent[] moveEvents = waitForEvent(fileObserver);
248         if (isEmulated) {
249             assertEventsContains(testFile, expected, moveEvents);
250         } else {
251             assertEventsEquals(testFile, expected, moveEvents);
252         }
253     }
254 
testFileObserver()255     public void testFileObserver() throws Exception {
256         helpTestFileObserver(getContext().getFilesDir(), false);
257     }
258 
259     @AppModeFull(reason = "Instant apps cannot access external storage")
testFileObserverExternal()260     public void testFileObserverExternal() throws Exception {
261         helpTestFileObserver(getContext().getExternalFilesDir(null), true);
262     }
263 
264     @AppModeFull(reason = "Instant apps cannot access external storage")
testFileObserverExternalStorageDirectory()265     public void testFileObserverExternalStorageDirectory() throws Exception {
266         helpTestFileObserver(EXT_STORAGE_DIR, true);
267     }
268 
269     @AppModeFull(reason = "Instant apps cannot access external storage")
testFileObserver_multipleFilesFull()270     public void testFileObserver_multipleFilesFull() throws Exception {
271         verifyMultipleFiles(
272                 Pair.create(getContext().getCacheDir(), false),
273                 Pair.create(getContext().getFilesDir(), false),
274                 Pair.create(getContext().getExternalFilesDir(null), true),
275                 Pair.create(EXT_STORAGE_DIR, true)
276         );
277     }
278 
279     @AppModeInstant(reason = "Instant specific variant excluding disallowed external storage")
testFileObserver_multipleFilesInstant()280     public void testFileObserver_multipleFilesInstant() throws Exception {
281         verifyMultipleFiles(
282                 Pair.create(getContext().getCacheDir(), false),
283                 Pair.create(getContext().getFilesDir(), false)
284         );
285     }
286 
287     @SafeVarargs
verifyMultipleFiles(Pair<File, Boolean>.... dirsAndIsEmulated)288     private final void verifyMultipleFiles(Pair<File, Boolean>... dirsAndIsEmulated) throws Exception {
289         List<File> directories = new ArrayList<>(dirsAndIsEmulated.length);
290         for (Pair<File, Boolean> pair : dirsAndIsEmulated) {
291             directories.add(pair.first);
292         }
293 
294         final MockFileObserver fileObserver1 = new MockFileObserver(directories);
295         try {
296             fileObserver1.startWatching();
297             for (Pair<File, Boolean> pair : dirsAndIsEmulated) {
298                 verifyTriggeredEventsOnFile(fileObserver1,
299                         new File(pair.first, TEST_FILE), pair.second);
300             }
301         } finally {
302             fileObserver1.stopWatching();
303         }
304 
305         directories = new ArrayList<>(dirsAndIsEmulated.length);
306         for (Pair<File, Boolean> pair : dirsAndIsEmulated) {
307             directories.add(new File(pair.first, TEST_DIR));
308         }
309 
310         final MockFileObserver fileObserver2 = new MockFileObserver(directories);
311         try {
312             fileObserver2.startWatching();
313             for (Pair<File, Boolean> pair : dirsAndIsEmulated) {
314                 verifyTriggeredEventsOnDir(fileObserver2,
315                         new File(pair.first, TEST_DIR), pair.second);
316             }
317         } finally {
318             fileObserver2.stopWatching();
319         }
320     }
321 
assertEventsEquals( File testFile, final int[] expected, final FileEvent[] moveEvents)322     private void assertEventsEquals(
323             File testFile, final int[] expected, final FileEvent[] moveEvents) {
324         List<Integer> expectedEvents = new ArrayList<Integer>();
325         for (int i = 0; i < expected.length; i++) {
326             expectedEvents.add(expected[i]);
327         }
328         List<FileEvent> actualEvents = Arrays.asList(moveEvents);
329         String message = "For test file [" + testFile.getAbsolutePath()
330                 + "] expected: " + expectedEvents + " Actual: " + actualEvents;
331         assertEquals(message, expected.length, moveEvents.length);
332         for (int i = 0; i < expected.length; i++) {
333             assertEquals(message, expected[i], moveEvents[i].event);
334         }
335     }
336 
assertEventsContains( File testFile, final int[] expected, final FileEvent[] moveEvents)337     private void assertEventsContains(
338             File testFile, final int[] expected, final FileEvent[] moveEvents) {
339         List<Integer> expectedEvents = new ArrayList<Integer>();
340         for (int i = 0; i < expected.length; i++) {
341             expectedEvents.add(expected[i]);
342         }
343         List<FileEvent> actualEvents = Arrays.asList(moveEvents);
344         String message = "For test file [" + testFile.getAbsolutePath()
345                 + "] expected: " + expectedEvents + " Actual: " + actualEvents;
346         int j = 0;
347         for (int i = 0; i < expected.length; i++) {
348             while (expected[i] != moveEvents[j].event) {
349                 j++;
350                 if (j >= moveEvents.length)
351                     fail(message);
352             }
353             j++;
354         }
355     }
356 
waitForEvent(MockFileObserver fileObserver)357     private FileEvent[] waitForEvent(MockFileObserver fileObserver)
358             throws InterruptedException {
359         Thread.sleep(DELAY_MSECOND);
360         synchronized (fileObserver) {
361             return fileObserver.getEvents();
362        }
363     }
364 
365     private static class FileEvent {
366         public int event = UNDEFINED;
367         public String path;
368 
FileEvent(final int event, final String path)369         public FileEvent(final int event, final String path) {
370             this.event = event;
371             this.path = path;
372         }
373 
374         @Override
toString()375         public String toString() {
376             return Integer.toString(event);
377         }
378     }
379 
380     /*
381      * MockFileObserver
382      */
383     private static class MockFileObserver extends FileObserver {
384 
385         private List<FileEvent> mEvents = new ArrayList<FileEvent>();
386 
MockFileObserver(File file)387         public MockFileObserver(File file) {
388             super(file);
389         }
390 
MockFileObserver(File file, int mask)391         public MockFileObserver(File file, int mask) {
392             super(file, mask);
393         }
394 
MockFileObserver(List<File> files)395         public MockFileObserver(List<File> files) {
396             super(files);
397         }
398 
MockFileObserver(List<File> files, int mask)399         public MockFileObserver(List<File> files, int mask) {
400             super(files, mask);
401         }
402 
403         @Override
onEvent(int event, String path)404         public synchronized void onEvent(int event, String path) {
405             mEvents.add(new FileEvent(event, path));
406         }
407 
getEvents()408         public synchronized FileEvent[] getEvents() {
409             final FileEvent[] events = new FileEvent[mEvents.size()];
410             mEvents.toArray(events);
411             mEvents.clear();
412             return events;
413         }
414     }
415 }
416