xref: /aosp_15_r20/cts/tests/tests/os/src/android/os/cts/FileObserverLegacyPathTest.java (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1 /*
2  * Copyright (C) 2020 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.content.ContentValues;
20 import android.content.Context;
21 import android.net.Uri;
22 import android.os.ConditionVariable;
23 import android.os.FileObserver;
24 import android.platform.test.annotations.AppModeFull;
25 import android.platform.test.annotations.AppModeSdkSandbox;
26 import android.provider.MediaStore;
27 import android.test.AndroidTestCase;
28 import java.io.File;
29 import java.io.OutputStream;
30 import java.util.HashMap;
31 import java.util.HashSet;
32 import java.util.Map;
33 
34 @AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).")
35 public class FileObserverLegacyPathTest extends AndroidTestCase {
36     ConditionVariable mCond;
37     Context mContext;
38     File mTestDir;
39 
40     @Override
setUp()41     protected void setUp() throws Exception {
42         mContext = getContext();
43         mCond = new ConditionVariable();
44 
45         mTestDir = new File("/sdcard/DCIM/testdir");
46         mTestDir.delete();
47         mTestDir.mkdirs();
48     }
49 
50     @Override
tearDown()51     protected void tearDown() throws Exception {
52         mTestDir.delete();
53     }
54 
55     /* This test creates a jpg image file and write some test data to that
56      * file.
57      * It verifies that FileObserver is able to catch the CREATE, OPEN and
58      * MODIFY events on that file, ensuring that, in the case of a FUSE mounted
59      * file system, changes applied to the lower file system will be detected
60      * by a monitored FUSE folder.
61      * Instead of checking if the set of generated events is exactly the same
62      * as the set of expected events, the test checks if the set of generated
63      * events contains CREATE, OPEN and MODIFY. This because there may be other
64      * services (e.g., file indexing) that may access the newly created file,
65      * generating spurious events that this test doesn't care of and filters
66      * them out. */
67     @AppModeFull(reason = "Instant apps cannot access external storage")
testCreateFile()68     public void testCreateFile() throws Exception {
69         String imageName = "image" + System.currentTimeMillis() + ".jpg";
70 
71         final Integer eventsMask = FileObserver.OPEN | FileObserver.CREATE | FileObserver.MODIFY;
72         PathFileObserver fileObserver =
73                 new PathFileObserver(mTestDir, eventsMask, mCond, Map.of(imageName, eventsMask));
74         fileObserver.startWatching();
75 
76         ContentValues cv = new ContentValues();
77         cv.put(MediaStore.Files.FileColumns.DISPLAY_NAME, imageName);
78         cv.put(MediaStore.Files.FileColumns.RELATIVE_PATH, "DCIM/testdir");
79         cv.put(MediaStore.Files.FileColumns.MIME_TYPE, "image/jpg");
80 
81         Uri imageUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL);
82 
83         Uri fileUri = mContext.getContentResolver().insert(imageUri, cv);
84 
85         OutputStream os = mContext.getContentResolver().openOutputStream(fileUri);
86         os.write("TEST".getBytes("UTF-8"));
87         os.close();
88 
89         /* Wait for for the inotify events to be caught. A timeout occurs after
90          * 2 seconds. */
91         mCond.block(2000);
92 
93         int detectedEvents = fileObserver.getEvents().getOrDefault(imageName, 0);
94 
95         /* Verify if the received events correspond to the ones that were requested */
96         assertEquals("Expected and received inotify events do not match",
97             PathFileObserver.eventsToSet(eventsMask),
98             PathFileObserver.eventsToSet(detectedEvents & eventsMask));
99 
100         fileObserver.stopWatching();
101     }
102 
103     static public class PathFileObserver extends FileObserver {
104         Map<String, Integer> mGeneratedEventsMap;
105         Map<String, Integer> mMonitoredEventsMap;
106         final ConditionVariable mCond;
107         final int mEventsMask;
108 
PathFileObserver(final File root, final int mask, ConditionVariable condition, Map<String, Integer> monitoredFiles)109         public PathFileObserver(final File root, final int mask, ConditionVariable condition,
110                 Map<String, Integer> monitoredFiles) {
111             super(root, FileObserver.ALL_EVENTS);
112 
113             mEventsMask = mask;
114             mCond = condition;
115             mGeneratedEventsMap = new HashMap<>();
116             mMonitoredEventsMap = monitoredFiles;
117         }
118 
getEvents()119         public Map<String, Integer> getEvents() { return mGeneratedEventsMap; }
120 
onEvent(final int event, final String path)121         public void onEvent(final int event, final String path) {
122             /* There might be some extra flags introduced by inotify.h.  Remove
123              * them. */
124             final int filteredEvent = event & FileObserver.ALL_EVENTS;
125             if (filteredEvent == 0)
126                 return;
127 
128             /* Update the event bitmap of the associated file. */
129             mGeneratedEventsMap.put(
130                     path, filteredEvent | mGeneratedEventsMap.getOrDefault(path, 0));
131 
132             /* Release the condition variable only if at least all the matching
133              * events have been caught for every monitored file. */
134             for (String file : mMonitoredEventsMap.keySet()) {
135                 int monitoredEvents = mMonitoredEventsMap.getOrDefault(file, 0);
136                 int generatedEvents = mGeneratedEventsMap.getOrDefault(file, 0);
137 
138                 if ((generatedEvents & monitoredEvents) != monitoredEvents)
139                     return;
140             }
141 
142             mCond.open();
143         }
144 
eventsToSet(int events)145         static public HashSet<String> eventsToSet(int events) {
146             HashSet<String> set = new HashSet<String>();
147             while (events != 0) {
148                 int lowestEvent = Integer.lowestOneBit(events);
149 
150                 set.add(event2str(lowestEvent));
151                 events &= ~lowestEvent;
152             }
153             return set;
154         }
155 
event2str(int event)156         static public String event2str(int event) {
157             switch (event) {
158                 case FileObserver.ACCESS:
159                     return "ACCESS";
160                 case FileObserver.ATTRIB:
161                     return "ATTRIB";
162                 case FileObserver.CLOSE_NOWRITE:
163                     return "CLOSE_NOWRITE";
164                 case FileObserver.CLOSE_WRITE:
165                     return "CLOSE_WRITE";
166                 case FileObserver.CREATE:
167                     return "CREATE";
168                 case FileObserver.DELETE:
169                     return "DELETE";
170                 case FileObserver.DELETE_SELF:
171                     return "DELETE_SELF";
172                 case FileObserver.MODIFY:
173                     return "MODIFY";
174                 case FileObserver.MOVED_FROM:
175                     return "MOVED_FROM";
176                 case FileObserver.MOVED_TO:
177                     return "MOVED_TO";
178                 case FileObserver.MOVE_SELF:
179                     return "MOVE_SELF";
180                 case FileObserver.OPEN:
181                     return "OPEN";
182                 default:
183                     return "???";
184             }
185         }
186     }
187 }
188