1package eu.siacs.conversations.utils;
2
3
4import android.os.FileObserver;
5import android.util.Log;
6
7import java.io.File;
8import java.util.ArrayList;
9import java.util.Collections;
10import java.util.List;
11import java.util.Stack;
12import java.util.concurrent.Executor;
13import java.util.concurrent.Executors;
14import java.util.concurrent.atomic.AtomicBoolean;
15
16import eu.siacs.conversations.Config;
17
18/**
19 * Copyright (C) 2012 Bartek Przybylski
20 * Copyright (C) 2015 ownCloud Inc.
21 * Copyright (C) 2016 Daniel Gultsch
22 */
23
24public abstract class ConversationsFileObserver {
25 private static final Executor EVENT_EXECUTOR = Executors.newSingleThreadExecutor();
26 private static final int MASK = FileObserver.DELETE | FileObserver.MOVED_FROM | FileObserver.CREATE;
27
28
29 private final String path;
30 private final List<SingleFileObserver> mObservers = new ArrayList<>();
31 private final AtomicBoolean shouldStop = new AtomicBoolean(true);
32
33 protected ConversationsFileObserver(String path) {
34 this.path = path;
35 }
36
37 public void startWatching() {
38 shouldStop.set(false);
39 startWatchingInternal();
40 }
41
42 private synchronized void startWatchingInternal() {
43 final Stack<String> stack = new Stack<>();
44 stack.push(path);
45
46 while (!stack.empty()) {
47 if (shouldStop.get()) {
48 Log.d(Config.LOGTAG, "file observer received command to stop");
49 return;
50 }
51 final String parent = stack.pop();
52 final File path = new File(parent);
53 mObservers.add(new SingleFileObserver(path, MASK));
54 final File[] files = path.listFiles();
55 for (final File file : (files == null ? new File[0] : files)) {
56 if (shouldStop.get()) {
57 Log.d(Config.LOGTAG, "file observer received command to stop");
58 return;
59 }
60 if (file.isDirectory() && file.getName().charAt(0) != '.') {
61 final String currentPath = file.getAbsolutePath();
62 if (depth(file) <= 8 && !stack.contains(currentPath) && !observing(file)) {
63 stack.push(currentPath);
64 }
65 }
66 }
67 }
68 for (FileObserver observer : mObservers) {
69 observer.startWatching();
70 }
71 }
72
73 private static int depth(File file) {
74 int depth = 0;
75 while ((file = file.getParentFile()) != null) {
76 depth++;
77 }
78 return depth;
79 }
80
81 private boolean observing(final File path) {
82 for (final SingleFileObserver observer : mObservers) {
83 if (path.equals(observer.path)) {
84 return true;
85 }
86 }
87 return false;
88 }
89
90 public void stopWatching() {
91 shouldStop.set(true);
92 stopWatchingInternal();
93 }
94
95 private synchronized void stopWatchingInternal() {
96 for (FileObserver observer : mObservers) {
97 observer.stopWatching();
98 }
99 mObservers.clear();
100 }
101
102 abstract public void onEvent(final int event, File path);
103
104 public void restartWatching() {
105 stopWatching();
106 startWatching();
107 }
108
109 private class SingleFileObserver extends FileObserver {
110 private final File path;
111
112 SingleFileObserver(final File path, final int mask) {
113 super(path.getAbsolutePath(), mask);
114 this.path = path;
115 }
116
117 @Override
118 public void onEvent(final int event, final String filename) {
119 if (filename == null) {
120 Log.d(Config.LOGTAG, "ignored file event with NULL filename (event=" + event + ")");
121 return;
122 }
123 EVENT_EXECUTOR.execute(() -> {
124 final File file = new File(this.path, filename);
125 if ((event & FileObserver.ALL_EVENTS) == FileObserver.CREATE) {
126 if (file.isDirectory()) {
127 Log.d(Config.LOGTAG, "file observer observed new directory creation " + file);
128 if (!observing(file)) {
129 final SingleFileObserver observer = new SingleFileObserver(file, MASK);
130 observer.startWatching();
131 }
132 }
133 return;
134 }
135 ConversationsFileObserver.this.onEvent(event, file);
136 });
137 }
138 }
139}