Thursday, March 24, 2011

Recursive (Sub directory) File System Watcher in Android.

Today, I was looking into monitoring directory changes in Android. It looks like FileObserver class in Android does not support recursive sub-directory watching even though it uses Linux internal "inotify".

Android documentations says:

Each FileObserver instance monitors a single file or directory. If a directory is monitored, events will be triggered for all files and subdirectories (recursively) inside the monitored directory.

But it does not work this way.

Anyway, then I looked in more and found
here it's been reported to Google and they have fixed it in 2.3 or 3.0 it seems. I tried to integrate the same code into my code and it failed because it's using Native functions.

Then I found this code on here with few compile errors, then i fixed it and thought of posting it here, because it might help someone else time.

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import android.os.FileObserver;
import android.util.Log;


public class RecursiveFileObserver extends FileObserver
{
/** Only modification events */
public static int CHANGES_ONLY = CREATE | DELETE | CLOSE_WRITE | MOVE_SELF
| MOVED_FROM | MOVED_TO;

List mObservers;
String mPath;
int mMask;

public RecursiveFileObserver(String path)
{
this(path, ALL_EVENTS);
}

public RecursiveFileObserver(String path, int mask)
{
super(path, mask);
mPath = path;
mMask = mask;
}

@Override
public void startWatching()
{
if (mObservers != null)
return ;

mObservers = new ArrayList();
Stack stack = new Stack();
stack.push(mPath);

while (!stack.isEmpty())
{
String parent = String.valueOf(stack.pop());
mObservers.add(new SingleFileObserver(parent, mMask));
File path = new File(parent);
File[]files = path.listFiles();
if (null == files)
continue;
for (File f: files)
{
if (f.isDirectory() && !f.getName().equals(".") && !f.getName()
.equals(".."))
{
stack.push(f.getPath());
}
}
}

for (SingleFileObserver sfo: mObservers)
{
sfo.startWatching();
}
}

@Override
public void stopWatching()
{
if (mObservers == null)
return ;

for (SingleFileObserver sfo: mObservers)
{
sfo.stopWatching();
}
mObservers.clear();
mObservers = null;
}

@Override
public void onEvent(int event, String path)
{
switch (event)
{
case FileObserver.ACCESS:
Log.i("RecursiveFileObserver", "ACCESS: " + path);
break;
case FileObserver.ATTRIB:
Log.i("RecursiveFileObserver", "ATTRIB: " + path);
break;
case FileObserver.CLOSE_NOWRITE:
Log.i("RecursiveFileObserver", "CLOSE_NOWRITE: " + path);
break;
case FileObserver.CLOSE_WRITE:
Log.i("RecursiveFileObserver", "CLOSE_WRITE: " + path);
break;
case FileObserver.CREATE:
Log.i("RecursiveFileObserver", "CREATE: " + path);
break;
case FileObserver.DELETE:
Log.i("RecursiveFileObserver", "DELETE: " + path);
break;
case FileObserver.DELETE_SELF:
Log.i("RecursiveFileObserver", "DELETE_SELF: " + path);
break;
case FileObserver.MODIFY:
Log.i("RecursiveFileObserver", "MODIFY: " + path);
break;
case FileObserver.MOVE_SELF:
Log.i("RecursiveFileObserver", "MOVE_SELF: " + path);
break;
case FileObserver.MOVED_FROM:
Log.i("RecursiveFileObserver", "MOVED_FROM: " + path);
break;
case FileObserver.MOVED_TO:
Log.i("RecursiveFileObserver", "MOVED_TO: " + path);
break;
case FileObserver.OPEN:
Log.i("RecursiveFileObserver", "OPEN: " + path);
break;
default:
Log.i("RecursiveFileObserver", "DEFAULT(" + event + ";) : " +
path);
break;
}
}

/**
* Monitor single directory and dispatch all events to its parent, with full path.
*/
class SingleFileObserver extends FileObserver
{
String mPath;

public SingleFileObserver(String path)
{
this(path, ALL_EVENTS);
mPath = path;
}

public SingleFileObserver(String path, int mask)
{
super(path, mask);
mPath = path;
}

@Override public void onEvent(int event, String path)
{
String newPath = mPath + "/" + path;
RecursiveFileObserver.this.onEvent(event, newPath);
}
}
}


How to use
-----------

RecursiveFileObserver mObserver;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

File DIRECTORY_DCIM = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);

mObserver = new RecursiveFileObserver(DIRECTORY_PICTURES.getAbsolutePath(), RecursiveFileObserver.CREATE);
mObserver.startWatching();

Toast.makeText(getApplicationContext(), "Watching " + DIRECTORY_PICTURES.getAbsolutePath(), Toast.LENGTH_LONG).show();
}


Android 0, Me 1 :)

11 comments:

  1. I'm glad they fixed it - I recently worked on an anti-virus project and ended up having to add a FileObserver to every single directory on the device.

    Barry

    ReplyDelete
  2. Looks like they are not going to accept this change because of overheads.

    ReplyDelete
  3. tyvm for posting!

    ReplyDelete
  4. There are some mistakes in your code, List and Stack are without class

    ReplyDelete
  5. Thanks this helps. Too bad Google doesn't care too much.

    ReplyDelete
  6. Did you get the "Type mismatch..." error when doing this:

    for (SingleFileObserver sfo: mObservers)
    {
    sfo.startWatching();
    }

    ReplyDelete
  7. I had tried this example on monitoring the change of /proc/net/tcp.. But it seems it doesn't work. It's there any suggestion about it?

    ReplyDelete
  8. One issue with your code is that it should itself react to changes to the directory. If a directory gets created it should add an observer to the directory. Likewise if a directory is removed the file observer should be removed.

    ReplyDelete
  9. Would also suggest using the listFiles call that takes a FileFilter parameter as it excludes the . and .. for you. If you have the filter only accept directories then the array returned will be just the subdirectories.

    Also Stack class is usually a wrong choice as it is synchronized because it is a subclass of Vector (it really should be deprecated). ArrayDeque or LinkedList would probably be a better choice.

    ReplyDelete
  10. Your code does not compile without changes, even though some commenters already pointed those problems out.

    Then, the switch block does not work either, because the event is not coming in a clean form, something like "if ((event & CREATE) != 0)" is needed instead.

    Thanks for the basic idea, it would be great if it worked out of the box!

    ReplyDelete
  11. Can we keep observer running all the time in background using a service class

    ReplyDelete