package org.emdev.common.filesystem;
import static android.os.FileObserver.CLOSE_WRITE;
import static android.os.FileObserver.CREATE;
import static android.os.FileObserver.DELETE;
import static android.os.FileObserver.MOVED_FROM;
import static android.os.FileObserver.MOVED_TO;
import android.app.Activity;
import android.os.FileObserver;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicBoolean;
import org.emdev.common.cache.CacheManager;
import org.emdev.common.log.LogContext;
import org.emdev.common.log.LogManager;
import org.emdev.ui.actions.EventDispatcher;
import org.emdev.ui.actions.InvokationType;
import org.emdev.ui.tasks.AsyncTask;
import org.emdev.utils.FileUtils;
import org.emdev.utils.LengthUtils;
import org.emdev.utils.StringUtils;
public class FileSystemScanner {
private static final LogContext LCTX = LogManager.root().lctx("FileSystemScanner", false);
private static final int EVENT_MASK = CREATE | CLOSE_WRITE | MOVED_TO | DELETE | MOVED_FROM;
final EventDispatcher listeners;
final AtomicBoolean inScan = new AtomicBoolean();
final Map<File, FileObserver> observers = new HashMap<File, FileObserver>();
private ScanTask m_scanTask;
public FileSystemScanner(final Activity activity) {
this.listeners = new EventDispatcher(activity, InvokationType.AsyncUI, Listener.class, ProgressListener.class);
}
public void shutdown() {
stopScan();
stopObservers();
}
public void startScan(final FileExtensionFilter filter, final String... paths) {
if (inScan.compareAndSet(false, true)) {
m_scanTask = new ScanTask(filter);
m_scanTask.execute(paths);
} else {
m_scanTask.addPaths(paths);
}
}
public void startScan(final FileExtensionFilter filter, final Collection<String> paths) {
final String[] arr = paths.toArray(new String[paths.size()]);
if (inScan.compareAndSet(false, true)) {
m_scanTask = new ScanTask(filter);
m_scanTask.execute(arr);
} else {
m_scanTask.addPaths(arr);
}
}
public boolean isScan() {
return inScan.get();
}
public void stopScan() {
if (inScan.compareAndSet(true, false)) {
m_scanTask = null;
}
}
public FileObserver getObserver(final File dir) {
// final String path = dir.getAbsolutePath();
synchronized (observers) {
// FileObserver fo = observers.get(path);
FileObserver fo = observers.get(dir);
if (fo == null) {
fo = new FileObserverImpl(dir);
observers.put(dir, fo);
}
return fo;
}
}
public void removeObserver(final File dir) {
synchronized (observers) {
observers.remove(dir);
}
}
public void stopObservers() {
synchronized (observers) {
for (final FileObserver o : observers.values()) {
o.stopWatching();
}
observers.clear();
}
}
public void stopObservers(final String path) {
final String mpath = FileUtils.invertMountPrefix(path);
final String ap = path + "/";
final String mp = mpath != null ? mpath + "/" : null;
synchronized (observers) {
final Iterator<Entry<File, FileObserver>> iter = observers.entrySet().iterator();
while (iter.hasNext()) {
final Entry<File, FileObserver> next = iter.next();
final File file = next.getKey();
final String filePath = file.getAbsolutePath();
final boolean eq = filePath.startsWith(ap) || filePath.equals(path) || mpath != null
&& (filePath.startsWith(mp) || filePath.equals(mpath));
if (eq) {
next.getValue().stopWatching();
iter.remove();
}
}
}
}
public void addListener(final Object listener) {
listeners.addListener(listener);
}
public void removeListener(final Object listener) {
listeners.removeListener(listener);
}
public static String toString(final int event) {
switch (event) {
case FileObserver.ACCESS:
return "ACCESS";
case FileObserver.MODIFY:
return "MODIFY";
case FileObserver.ATTRIB:
return "ATTRIB";
case FileObserver.CLOSE_WRITE:
return "CLOSE_WRITE";
case FileObserver.CLOSE_NOWRITE:
return "CLOSE_NOWRITE";
case FileObserver.OPEN:
return "OPEN";
case FileObserver.MOVED_FROM:
return "MOVED_FROM";
case FileObserver.MOVED_TO:
return "MOVED_TO";
case FileObserver.CREATE:
return "CREATE";
case FileObserver.DELETE:
return "DELETE";
case FileObserver.DELETE_SELF:
return "DELETE_SELF";
case FileObserver.MOVE_SELF:
return "MOVE_SELF";
default:
return "0x" + Integer.toHexString(event);
}
}
class ScanTask extends AsyncTask<String, String, Void> {
final FileExtensionFilter filter;
final LinkedList<File> paths = new LinkedList<File>();
public ScanTask(final FileExtensionFilter filter) {
this.filter = filter;
}
@Override
protected void onPreExecute() {
final ProgressListener pl = listeners.getListener();
pl.showProgress(true);
}
@Override
protected Void doInBackground(final String... paths) {
addPaths(paths);
try {
for (File dir = getDir(); dir != null && inScan.get(); dir = getDir()) {
scanDir(dir);
}
} finally {
inScan.set(false);
}
return null;
}
@Override
protected void onPostExecute(final Void v) {
final ProgressListener pl = listeners.getListener();
pl.showProgress(false);
}
void scanDir(final File dir) {
// Checks if scan should be continued
if (!inScan.get()) {
return;
}
if (dir == null || !dir.isDirectory()) {
return;
}
if (dir.getAbsolutePath().startsWith("/sys")) {
LCTX.d("Skip system dir: " + dir);
return;
}
try {
final File cd = CacheManager.getCacheDir();
if (cd != null && dir.getCanonicalPath().equals(cd.getCanonicalPath())) {
LCTX.d("Skip file cache: " + dir);
return;
}
} catch (final IOException ex) {
ex.printStackTrace();
}
if (LCTX.isDebugEnabled()) {
LCTX.d("Scan dir: " + dir);
}
// Retrieves file observer for scanning folder
final FileObserver observer = getObserver(dir);
// Stop watching
observer.stopWatching();
// Retrieves listener
final Listener l = listeners.getListener();
// Retrieves file list
final File[] files = dir.listFiles((FilenameFilter) filter);
// Sort file list
if (LengthUtils.isNotEmpty(files)) {
Arrays.sort(files, StringUtils.NFC);
}
// Call the file scan callback
l.onFileScan(dir, files);
// Retrieves files from current directory
final File[] childDirs = dir.listFiles(DirectoryFilter.ALL);
// Immediately starts folder watching
getObserver(dir).startWatching();
if (LengthUtils.isNotEmpty(childDirs)) {
// Sort child dir list
Arrays.sort(childDirs, StringUtils.NFC);
// Add children for deep ordered scanning
synchronized (this) {
for (int i = childDirs.length - 1; i >= 0; i--) {
this.paths.addFirst(childDirs[i]);
}
}
}
}
synchronized void addPaths(final String... paths) {
for (final String path : paths) {
final File dir = new File(path);
if (dir.exists() && dir.isDirectory()) {
this.paths.add(dir);
}
}
}
synchronized File getDir() {
return this.paths.isEmpty() ? null : this.paths.removeFirst();
}
}
class FileObserverImpl extends FileObserver {
private final File folder;
public FileObserverImpl(final File folder) {
super(folder.getAbsolutePath(), EVENT_MASK);
this.folder = folder;
}
@Override
public void onEvent(final int event, final String path) {
if (folder == null || path == null) {
return;
}
final File f = new File(folder, path);
final boolean isDirectory = f.isDirectory();
final Listener l = listeners.getListener();
int actualEvent = event & ALL_EVENTS;
LCTX.d("0x" + Integer.toHexString(event) + " " + FileSystemScanner.toString(actualEvent) + ": "
+ f.getAbsolutePath());
switch (actualEvent) {
case CREATE:
if (isDirectory) {
l.onDirAdded(folder, f);
getObserver(f).startWatching();
} else {
// Ignore file creation, wait for data writing
}
break;
case CLOSE_WRITE:
case MOVED_TO:
if (isDirectory) {
l.onDirAdded(folder, f);
getObserver(f).startWatching();
} else {
l.onFileAdded(folder, f);
}
break;
case DELETE:
case MOVED_FROM:
if (isDirectory) {
l.onDirDeleted(folder, f);
removeObserver(f);
} else {
l.onFileDeleted(folder, f);
}
break;
default:
break;
}
}
}
public static interface Listener {
void onFileScan(File parent, File[] files);
void onFileAdded(File parent, File f);
void onFileDeleted(File parent, File f);
void onDirAdded(File parent, File f);
void onDirDeleted(File parent, File f);
}
public static interface ProgressListener {
public void showProgress(boolean show);
}
}