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