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