• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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