19

I am trying to monitor a folder on the users disk in the background, much like how you can monitor gallery changes with a JobScheduler and contentobserver. I want to do this for any specified directory. However, I cannot figure out how to receive a broadcast when the directory has any file changes.

Phantômaxx
  • 37,901
  • 21
  • 84
  • 115
Dreamers Org
  • 1,151
  • 3
  • 12
  • 30
  • 2
    `android.os.FileObserver` ? – pskink Dec 30 '17 at 14:25
  • Give an example on how to trigger this in background automatically @pskink – Dreamers Org Dec 30 '17 at 14:27
  • 2
    did you read the javadoc documentation? if so, what do you have problems with? – pskink Dec 30 '17 at 14:28
  • it does not show how to have code triggered in background to receive all directory changes @pskink – Dreamers Org Dec 30 '17 at 14:32
  • 2
    use `Service`s then – pskink Dec 30 '17 at 14:36
  • give an examples @pskink – Dreamers Org Dec 30 '17 at 14:36
  • 2
    read `android.app.Service` javadocs – pskink Dec 30 '17 at 14:38
  • I have. please give example on how to schedule code to be ran on every file change when in background. A service would just keep running which is good for battery life. – Dreamers Org Dec 30 '17 at 14:40
  • 2
    so what have you done so far with FileObserver? – pskink Dec 30 '17 at 14:42
  • This example handles something different but will be a good guide on how it's done: https://stackoverflow.com/a/6592308/2979092 – WLGfx Jan 10 '18 at 16:49
  • 2
    after every x interval read all the files and folder detail from a specific directory using `Service` or `JobDispatcher` and stored that details in local database next time compare new files the result with old file details and update the local database. when an application is running use ` android.os.FileObserver` for file changes. – Om Infowave Developers Jan 11 '18 at 09:53
  • 1
    Android tries very hard to hide the underlying file system so don't expect any support in this direction. If you want to use `FileObserver` you'll have to run it yourself, perhaps in a foreground service, and expose its notifications using a custom content provider. that way you can use `JobScheduler` to observe changes as you're used to. – Eugen Pechanec Jan 11 '18 at 11:08

2 Answers2

23

There are several things you have to do in order create a system wide file observer.

Firstly

You have to create a service that will start up at boot and will always be running. In order for this to happen you have to create a BroadcastReceiver, register it to receive ACTION_BOOT_COMPLETED and the RECEIVE_BOOT_COMPLETED permission to your Manifest

public class StartupReceiver extends BroadcastReceiver {   

    @Override
    public void onReceive(Context context, Intent intent) {

     Intent myIntent = new Intent(context, FileSystemObserverService.class);
     context.startService(myIntent);

    }
}

In your Manifest

<manifest >
    <application >

    <service
        android:name=".FileSystemObserverService"
        android:enabled="true"
        android:exported="true" >
    </service>
    <!-- Declaring broadcast receiver for BOOT_COMPLETED event. -->
        <receiver android:name=".StartupReceiver" android:enabled="true" android:exported="false">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
            </intent-filter>
        </receiver>

    </application>

    <!-- Adding the permission -->
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

</manifest>

Secondly

The service has to implement the android.os.FileObserver class. As android.os.FileObserver only observes the path you send to it and does not observe the path's subdirectories, you have to extend it and add SingleFileObserver observers to it. You also have to run the observation in another thread with priority set to low

        public class FileSystemObserverService extends Service {

 @Override
 public IBinder onBind(Intent intent) {
     // TODO: Return the communication channel to the service.
     throw new UnsupportedOperationException("Not yet implemented");
 }

 @Override
 public int onStartCommand(Intent intent, int flags, int startId) {
     observe();
     return super.onStartCommand(intent, flags, startId);
 }

 public File getInternalStoragePath() {
     File parent = Environment.getExternalStorageDirectory().getParentFile();
     File external = Environment.getExternalStorageDirectory();
     File[] files = parent.listFiles();
     File internal = null;
     if (files != null) {
         for (int i = 0; i < files.length; i++) {
             if (files[i].getName().toLowerCase().startsWith("sdcard") && !files[i].equals(external)) {
                 internal = files[i];
             }
         }
     }

     return internal;
 }
 public File getExtenerStoragePath() {

     return Environment.getExternalStorageDirectory();
 }

 public void observe() {
     Thread t = new Thread(new Runnable() {

         @Override
         public void run() {


             //File[]   listOfFiles = new File(path).listFiles();
             File str = getInternalStoragePath();
             if (str != null) {
                 internalPath = str.getAbsolutePath();

                 new Obsever(internalPath).startWatching();
             }
             str = getExtenerStoragePath();
             if (str != null) {

                 externalPath = str.getAbsolutePath();
                 new Obsever(externalPath).startWatching();
             }



         }
     });
     t.setPriority(Thread.MIN_PRIORITY);
     t.start();


 }

 class Obsever extends FileObserver {

     List < SingleFileObserver > mObservers;
     String mPath;
     int mMask;
     public Obsever(String path) {
         // TODO Auto-generated constructor stub
         this(path, ALL_EVENTS);
     }
     public Obsever(String path, int mask) {
         super(path, mask);
         mPath = path;
         mMask = mask;
         // TODO Auto-generated constructor stub

     }
     @Override
     public void startWatching() {
         // TODO Auto-generated method stub
         if (mObservers != null)
             return;
         mObservers = new ArrayList < SingleFileObserver > ();
         Stack < String > stack = new Stack < String > ();
         stack.push(mPath);
         while (!stack.empty()) {
             String parent = stack.pop();
             mObservers.add(new SingleFileObserver(parent, mMask));
             File path = new File(parent);
             File[] files = path.listFiles();
             if (files == null) continue;
             for (int i = 0; i < files.length; ++i) {
                 if (files[i].isDirectory() && !files[i].getName().equals(".") && !files[i].getName().equals("..")) {
                     stack.push(files[i].getPath());
                 }
             }
         }
         for (int i = 0; i < mObservers.size(); i++) {
             mObservers.get(i).startWatching();
         }
     }
     @Override
     public void stopWatching() {
         // TODO Auto-generated method stub
         if (mObservers == null)
             return;
         for (int i = 0; i < mObservers.size(); ++i) {
             mObservers.get(i).stopWatching();
         }
         mObservers.clear();
         mObservers = null;
     }
     @Override
     public void onEvent(int event, final String path) {
         if (event == FileObserver.OPEN) {
             //do whatever you want
         } else if (event == FileObserver.CREATE) {
             //do whatever you want
         } else if (event == FileObserver.DELETE_SELF || event == FileObserver.DELETE) {

             //do whatever you want
         } else if (event == FileObserver.MOVE_SELF || event == FileObserver.MOVED_FROM || event == FileObserver.MOVED_TO) {
             //do whatever you want

         }
     }

     private class SingleFileObserver extends FileObserver {
         private String mPath;
         public SingleFileObserver(String path, int mask) {
             super(path, mask);
             // TODO Auto-generated constructor stub
             mPath = path;
         }

         @Override
         public void onEvent(int event, String path) {
             // TODO Auto-generated method stub
             String newPath = mPath + "/" + path;
             Obsever.this.onEvent(event, newPath);
         }

     }

 }

That's it With this code you'll be able to observe the entire file system, both the internal and external file systems

TimWeri
  • 25
  • 5
Niza Siwale
  • 2,390
  • 1
  • 18
  • 20
  • 5
    Won't the service be eventually killed by the system if it isn't running as a foreground service? – Mark Pazon Feb 26 '18 at 10:38
  • I may certainly be missing something, but there seem to be a couple of issues (1) if the service is started more than once then it appears you'll end up with multiple threads watching the same files with no way of stopping them since no references appear to be getting held and (2) what is the scope of these "Obsever" instances - there does not appear to be any references to them held anywhere so can they not be garbage collected at any moment? – Thomas Sunderland May 21 '19 at 16:41
  • The FileObserver **will** observe the path's subdirectories, as https://developer.android.com/reference/android/os/FileObserver wrote: Each FileObserver instance can monitor multiple files or directories. If a directory is monitored, events will be triggered for all files and subdirectories inside the monitored directory. – LeeR Mar 03 '20 at 08:14
  • Is Android using the Linux inotify module for this? – daparic May 20 '20 at 20:12
  • Yes, such service will be easily killed by system. You can create foreground service with notification, then it should survive and possibly be restarted by system if it's killed. – Pointer Null Sep 16 '20 at 19:02
5

Just start a service that uses a Fileobserver to detect the file changes for the specified directory.

The service class:

public class FileObserverService extends Service {
    private FileObserver mFileObserver;

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if((intent.hasExtra(INTENT_EXTRA_FILEPATH))) // we store the path of directory inside the intent that starts the service
        mFileObserver = new FileObserver(intent.getStringExtra(INTENT_EXTRA_FILEPATH)) {
            @Override
            public void onEvent(int event, String path) {
               // If an event happens we can do stuff inside here
               // for example we can send a broadcast message with the event-id          
               Log.d("FILEOBSERVER_EVENT", "Event with id " + Integer.toHexString(event) + " happened") // event identifies the occured Event in hex
            }
        };
        mFileObserver.startWatching(); // The FileObserver starts watching
        return Service.START_NOT_STICKY;
    }

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public IBinder onBind(Intent intent) {
        //TODO for communication return IBinder implementation
        return null;
    }
}

Start the service from somewhere inside your app with YOUR_FILEPATH/DIRECTORY you want to observe:

Intent intent = new Intent(this, FileObserverService.class);
    intent.putExtra(INTENT_EXTRA_FILEPATH, "YOUR_FILEPATH");
    this.startService(intent);
GoRoS
  • 5,183
  • 2
  • 43
  • 66
martin
  • 191
  • 7
  • this misses the part where i need it to run anytime there is a file change in this path. Can't have a service run 24/7 – Dreamers Org Jan 12 '18 at 12:16
  • If you can´t have a service running, there is no method for you to continuously monitor external storage operations, other than by modifying the firmware. Even a created service could bewhen low on resources. The other possibility would be a AlarmManager that periodically scans the directory for changes, with a polling period chosen by the user, so they can control battery and CPU consumption by your app. But than you are not able to detect the file changes immediately. – martin Jan 12 '18 at 20:27