1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.Q; 4 5 import android.os.FileObserver; 6 import java.io.File; 7 import java.io.IOException; 8 import java.nio.file.FileSystems; 9 import java.nio.file.Files; 10 import java.nio.file.Path; 11 import java.nio.file.StandardWatchEventKinds; 12 import java.nio.file.WatchEvent; 13 import java.nio.file.WatchKey; 14 import java.nio.file.WatchService; 15 import java.util.ArrayList; 16 import java.util.HashMap; 17 import java.util.HashSet; 18 import java.util.List; 19 import java.util.Map; 20 import java.util.Set; 21 import javax.annotation.concurrent.GuardedBy; 22 import org.robolectric.annotation.Implementation; 23 import org.robolectric.annotation.Implements; 24 import org.robolectric.annotation.RealObject; 25 26 /** 27 * A shadow implementation of FileObserver that uses java.nio.file.WatchService. 28 * 29 * <p>Currently only supports MODIFY, DELETE and CREATE (CREATE will encompass also events that 30 * would normally register as MOVED_FROM, and DELETE will encompass also events that would normally 31 * register as MOVED_TO). Other event types will be silently ignored. 32 */ 33 @Implements(FileObserver.class) 34 public class ShadowFileObserver { 35 @RealObject private FileObserver realFileObserver; 36 37 private final WatchService watchService; 38 private final Map<String, WatchedDirectory> watchedDirectories = new HashMap<>(); 39 private final Map<WatchKey, Path> watchedKeys = new HashMap<>(); 40 41 private WatchEvent.Kind<?>[] watchEvents = new WatchEvent.Kind<?>[0]; 42 43 @GuardedBy("this") 44 private WatcherRunnable watcherRunnable = null; 45 ShadowFileObserver()46 public ShadowFileObserver() { 47 try { 48 this.watchService = FileSystems.getDefault().newWatchService(); 49 } catch (IOException ioException) { 50 throw new RuntimeException(ioException); 51 } 52 } 53 54 @Override 55 @Implementation finalize()56 protected void finalize() throws Throwable { 57 stopWatching(); 58 } 59 setMask(int mask)60 private void setMask(int mask) { 61 Set<WatchEvent.Kind<Path>> watchEventsSet = new HashSet<>(); 62 if ((mask & FileObserver.MODIFY) != 0) { 63 watchEventsSet.add(StandardWatchEventKinds.ENTRY_MODIFY); 64 } 65 if ((mask & FileObserver.DELETE) != 0) { 66 watchEventsSet.add(StandardWatchEventKinds.ENTRY_DELETE); 67 } 68 if ((mask & FileObserver.CREATE) != 0) { 69 watchEventsSet.add(StandardWatchEventKinds.ENTRY_CREATE); 70 } 71 watchEvents = watchEventsSet.toArray(new WatchEvent.Kind<?>[0]); 72 } 73 addFile(File file)74 private void addFile(File file) { 75 List<File> list = new ArrayList<>(1); 76 list.add(file); 77 addFiles(list); 78 } 79 addFiles(List<File> files)80 private void addFiles(List<File> files) { 81 // Break all watched files into their directories. 82 for (File file : files) { 83 Path path = file.toPath(); 84 if (Files.isDirectory(path)) { 85 WatchedDirectory watchedDirectory = new WatchedDirectory(path); 86 watchedDirectories.put(path.toString(), watchedDirectory); 87 } else { 88 Path directory = path.getParent(); 89 String filename = path.getFileName().toString(); 90 WatchedDirectory watchedDirectory = watchedDirectories.get(directory.toString()); 91 if (watchedDirectory == null) { 92 watchedDirectory = new WatchedDirectory(directory); 93 } 94 watchedDirectory.addFile(filename); 95 watchedDirectories.put(directory.toString(), watchedDirectory); 96 } 97 } 98 } 99 100 @Implementation __constructor__(String path, int mask)101 protected void __constructor__(String path, int mask) { 102 setMask(mask); 103 addFile(new File(path)); 104 } 105 106 @Implementation(minSdk = Q) __constructor__(List<File> files, int mask)107 protected void __constructor__(List<File> files, int mask) { 108 setMask(mask); 109 addFiles(files); 110 } 111 112 /** 113 * Represents a directory to watch, including specific files in that directory (or the entire 114 * directory contents if no file is specified). 115 */ 116 private class WatchedDirectory { 117 @GuardedBy("this") 118 private WatchKey watchKey = null; 119 120 private final Path dirPath; 121 private final Set<String> watchedFiles = new HashSet<>(); 122 WatchedDirectory(Path dirPath)123 WatchedDirectory(Path dirPath) { 124 this.dirPath = dirPath; 125 } 126 addFile(String filename)127 void addFile(String filename) { 128 watchedFiles.add(filename); 129 } 130 register()131 synchronized void register() throws IOException { 132 unregister(); 133 this.watchKey = dirPath.register(watchService, watchEvents); 134 watchedKeys.put(watchKey, dirPath); 135 } 136 unregister()137 synchronized void unregister() { 138 if (this.watchKey != null) { 139 watchedKeys.remove(watchKey); 140 watchKey.cancel(); 141 this.watchKey = null; 142 } 143 } 144 } 145 146 @Implementation startWatching()147 protected synchronized void startWatching() throws IOException { 148 // If we're already watching, startWatching is a no-op. 149 if (watcherRunnable != null) { 150 return; 151 } 152 153 // If we don't have any supported events to watch for, don't do anything. 154 if (watchEvents.length == 0) { 155 return; 156 } 157 158 for (WatchedDirectory watchedDirectory : watchedDirectories.values()) { 159 watchedDirectory.register(); 160 } 161 162 watcherRunnable = 163 new WatcherRunnable(realFileObserver, watchedDirectories, watchedKeys, watchService); 164 Thread thread = new Thread(watcherRunnable, "ShadowFileObserver"); 165 thread.start(); 166 } 167 168 @Implementation stopWatching()169 protected void stopWatching() { 170 for (WatchedDirectory watchedDirectory : watchedDirectories.values()) { 171 watchedDirectory.unregister(); 172 } 173 174 synchronized (this) { 175 if (watcherRunnable != null) { 176 watcherRunnable.stop(); 177 watcherRunnable = null; 178 } 179 } 180 } 181 fileObserverEventFromWatcherEvent(WatchEvent.Kind<?> kind)182 private static int fileObserverEventFromWatcherEvent(WatchEvent.Kind<?> kind) { 183 if (kind == StandardWatchEventKinds.ENTRY_CREATE) { 184 return FileObserver.CREATE; 185 } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) { 186 return FileObserver.DELETE; 187 } else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) { 188 return FileObserver.MODIFY; 189 } 190 return 0; 191 } 192 193 /** Runnable implementation that processes all events for keys queued to the watcher. */ 194 private static class WatcherRunnable implements Runnable { 195 @GuardedBy("this") 196 private boolean shouldStop = false; 197 198 private final FileObserver realFileObserver; 199 private final Map<String, WatchedDirectory> watchedDirectories; 200 private final Map<WatchKey, Path> watchedKeys; 201 private final WatchService watchService; 202 WatcherRunnable( FileObserver realFileObserver, Map<String, WatchedDirectory> watchedDirectories, Map<WatchKey, Path> watchedKeys, WatchService watchService)203 public WatcherRunnable( 204 FileObserver realFileObserver, 205 Map<String, WatchedDirectory> watchedDirectories, 206 Map<WatchKey, Path> watchedKeys, 207 WatchService watchService) { 208 this.realFileObserver = realFileObserver; 209 this.watchedDirectories = watchedDirectories; 210 this.watchedKeys = watchedKeys; 211 this.watchService = watchService; 212 } 213 stop()214 public synchronized void stop() { 215 this.shouldStop = true; 216 } 217 shouldContinue()218 public synchronized boolean shouldContinue() { 219 return !shouldStop; 220 } 221 222 @SuppressWarnings("unchecked") castToPathWatchEvent(WatchEvent<?> untypedWatchEvent)223 private WatchEvent<Path> castToPathWatchEvent(WatchEvent<?> untypedWatchEvent) { 224 return (WatchEvent<Path>) untypedWatchEvent; 225 } 226 227 @Override run()228 public void run() { 229 while (shouldContinue()) { 230 // wait for key to be signalled 231 WatchKey key; 232 try { 233 key = watchService.take(); 234 } catch (InterruptedException x) { 235 return; 236 } 237 238 Path dir = watchedKeys.get(key); 239 if (dir != null) { 240 WatchedDirectory watchedDirectory = watchedDirectories.get(dir.toString()); 241 List<WatchEvent<?>> events = key.pollEvents(); 242 243 for (WatchEvent<?> event : events) { 244 WatchEvent.Kind<?> kind = event.kind(); 245 246 // Ignore OVERFLOW events 247 if (kind == StandardWatchEventKinds.OVERFLOW) { 248 continue; 249 } 250 251 WatchEvent<Path> ev = castToPathWatchEvent(event); 252 Path fileName = ev.context().getFileName(); 253 254 if (watchedDirectory.watchedFiles.isEmpty()) { 255 realFileObserver.onEvent( 256 fileObserverEventFromWatcherEvent(kind), fileName.toString()); 257 } else { 258 for (String watchedFile : watchedDirectory.watchedFiles) { 259 if (fileName.toString().equals(watchedFile)) { 260 realFileObserver.onEvent( 261 fileObserverEventFromWatcherEvent(kind), fileName.toString()); 262 } 263 } 264 } 265 } 266 } 267 boolean valid = key.reset(); 268 if (!valid) { 269 return; 270 } 271 } 272 } 273 } 274 } 275