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