How do I use FileObserver

  1. Don’t forget to apply for dynamic permissions in your code

     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
  2. You can implement FileObserver by specifying one or more files and directories, and you can also specify the events you care about.
   private val observer = object : FileObserver(dir.absolutePath, ALL_EVENTS) {
            override fun onEvent(event: Int, path: String?) {
                if (debug) {
                    Log.d("FileObserverManager", "onEvent event $event path $path")
                }
            }
        }
  1. Start listening and stop listening

        init {
            observer.startWatching()
            if (debug)
                Log.d("FileObserverManager", "init startWatching")
        }

        fun tryRelease() {
            observer.stopWatching()
            if (debug)
                Log.d("FileObserverManager", "tryRelease stopWatching")
        }

Problems encountered

The whole use process is relatively simple, there is no complex process, but sometimes found in the use of the process can not receive the event. In the process of using a number of screens need to monitor the path file changes, in the screen exit when the current screen to cancel the monitoring. Searched on the Internet for the problem that FileObserver could not receive the event, everyone solved the problem by holding the FileObserver globally, but my problem was still not solved. To solve the problem we have to look at the FileObserver source code to analyze the cause of the problem. The FileObserver source code does reveal something fishy.

   private static ObserverThread s_observerThread;

    static {
        s_observerThread = new ObserverThread();
        s_observerThread.start();
    }

This static observerThread should be the key to the problem, because it is static, so all FileObserver use the same observerThread to listen for changes to the file.

public void startWatching() { if (mDescriptors == null) { mDescriptors = s_observerThread.startWatching(mFiles, mMask, this); }}

FileObsever’s startWatching method is used to start listening, and when it starts listening it returns data to mDescriptors, so you can imagine that you can find the corresponding relationship of the file listening by mDescriptors. Let’s look at the code to stop listening:

    public void stopWatching() {
        if (mDescriptors != null) {
            s_observerThread.stopWatching(mDescriptors);
            mDescriptors = null;
        }
    }

We saw that when we stopped listening to files we passed the mDescriptors to ObserverThread, so it should be OK to think of mDescriptors as keeping the mapping of listening to files.

In the process of use, many interfaces will start and stop listening through FileObserver, so is it possible that the listening of the previous screen on the same path will be stopped after the exit of the later screen? Repeated testing has found that startup and shutdown of different FileObservers that are indeed the same path can affect each other.

How to solve the problem

Since the different FileObserver for each directory will affect each other, we can only maintain one FileObserver for each directory.

object FileObserverManager { private const val debug = false private val fileObserverWrappers = hashMapOf<String, FileObserverWrapper>() fun register(listener: FileEventListener) { var fileObserverWrapper = fileObserverWrappers[listener.dir.absolutePath] if (fileObserverWrapper == null) { fileObserverWrapper = FileObserverWrapper(listener.dir).apply { fileObserverWrappers[listener.dir.absolutePath] = this } } fileObserverWrapper.addFileEventListener(listener) } fun unregister(listener: FileEventListener) { val fileObserverWrapper = fileObserverWrappers[listener.dir.absolutePath] fileObserverWrapper? .removeFileEventListener(listener) if (fileObserverWrapper? .tryRelease() == true) { fileObserverWrappers.remove(listener.dir.absolutePath) } } private class FileObserverWrapper(dir: File) { private val fileEventListeners = mutableListOf<FileEventListener>() private val observer = object : FileObserver(dir.absolutePath, ALL_EVENTS) { override fun onEvent(event: Int, path: String?) { if (debug) { Log.d("FileObserverManager", "onEvent event $event path $path") } fileEventListeners.forEach { fileEventListener -> if (event.and(fileEventListener.eventMask) ! = 0) { fileEventListener.onEvent(event, path) } } } } init { observer.startWatching() if (debug) Log.d("FileObserverManager", "init startWatching") } fun addFileEventListener(listener: FileEventListener) { fileEventListeners.add(listener) if (debug) Log.d("FileObserverManager", "addFileEventListener ${fileEventListeners.size}") } fun removeFileEventListener(listener: FileEventListener) { fileEventListeners.remove(listener) if (debug) Log.d("FileObserverManager", "removeFileEventListener ${fileEventListeners.size}") } fun tryRelease(): Boolean { if (fileEventListeners.isEmpty()) { observer.stopWatching() if (debug) Log.d("FileObserverManager", "tryRelease stopWatching") return true } return false } } interface FileEventListener { val dir: File val eventMask: Int fun onEvent(event: Int, path: String?) }}

The FileObserverWrappers field maintains the mapping between the directory and FileObserver, and in addition to managing FileObserver in the FileObserverWrapper, it also manages different listens to the same directory.

fun register(listener: FileEventListener) { var fileObserverWrapper = fileObserverWrappers[listener.dir.absolutePath] if (fileObserverWrapper == null) { fileObserverWrapper = FileObserverWrapper(listener.dir).apply { fileObserverWrappers[listener.dir.absolutePath] = this } } fileObserverWrapper.addFileEventListener(listener) } fun unregister(listener: FileEventListener) { val fileObserverWrapper = fileObserverWrappers[listener.dir.absolutePath] fileObserverWrapper? .removeFileEventListener(listener) if (fileObserverWrapper? .tryRelease() == true) { fileObserverWrappers.remove(listener.dir.absolutePath) } }

These two methods register path listeners and delete path listeners, while FileObserverManager is singleton and maintains all listens for the application.

    interface FileEventListener {
        val dir: File
        val eventMask: Int
        fun onEvent(event: Int, path: String?)
    }

The FileEventListener interface defines the path to listen on and the event of interest. The following code shows how to define a listener for listening.

val fileEventListener = object : FileObserverManager.FileEventListener { override val dir: File get() = File("mnt/sdcard") override val eventMask: Int get() = FileObserver.CREATE.or(FileObserver.DELETE) override fun onEvent(event: Int, path: String?) { path ? : return Log.d("FileObserverManager", "onEvent $path") } }

FileEventListener listens for the directory “MNT /sdcard” and listens for creation and deletion events.

conclusion

  1. File permissions must be applied.
  2. ObserverThread is held as a static variable, so startup and cancellation of different FileObservers on the same path in the same process will affect each other.
  3. The FileObserverManager singleton object is used to maintain the FileObserver of the same path, ensuring the uniqueness of the FileObserver of the same path, which also resolves the conflict problem.