Multiple FileObserver on same file failed
Asked Answered
D

2

5

In my application, I have different components, which monitor the particular file in sdcard using FileObservers. So there are two instances of File Observer which observe a single file, say abc.xml for all events.

FileObserver fo1 = new FileObserver(new File("/sdcard/abc.xml"));
fo1.startWatching();
FileObserver fo2 = new FileObserver(new File("/sdcard/abc.xml"));
fo2.startWatching();

They both are registered for different events. My problem is when both of the file observers are watching in parallel, I am missing the calls to onEvent() of "fo1".

Is this a limitation of Android system? What are the ways to overcome this problem?

Dasie answered 24/3, 2015 at 7:55 Comment(1)
The same problem. I have first observer at Activity and second at Service. onEvent calls only at observer which latest call startWatchingDistilled
I
9

Late but maybe helpful for others: It's a bug in Android - the issue is reported here.

Since this was making me tear my hair out, I wrote a drop-in replacement for FileObserver which works around the issue by maintaining a master-list of FileObservers. Replacing all FileObservers in an application with this FixedFileObserver should result in the expected behaviour. (health warning: I did not test it very extensively in all corner cases but it works for me)

FixedFileObserver.java

package com.fimagena.filepicker.backend;

import android.os.FileObserver;

import java.io.File;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;


public abstract class FixedFileObserver {

    private final static HashMap<File, Set<FixedFileObserver>> sObserverLists = new HashMap<>();

    private FileObserver mObserver;
    private final File mRootPath;
    private final int mMask;

    public FixedFileObserver(String path) {this(path, FileObserver.ALL_EVENTS);}
    public FixedFileObserver(String path, int mask) {
        mRootPath = new File(path);
        mMask = mask;
    }

    public abstract void onEvent(int event, String path);

    public void startWatching() {
        synchronized (sObserverLists) {
            if (!sObserverLists.containsKey(mRootPath)) sObserverLists.put(mRootPath, new HashSet<FixedFileObserver>());

            final Set<FixedFileObserver> fixedObservers = sObserverLists.get(mRootPath);

            mObserver = fixedObservers.size() > 0 ? fixedObservers.iterator().next().mObserver : new FileObserver(mRootPath.getPath()) {
                @Override public void onEvent(int event, String path) {
                    for (FixedFileObserver fixedObserver : fixedObservers)
                        if ((event & fixedObserver.mMask) != 0) fixedObserver.onEvent(event, path);
                }};
            mObserver.startWatching();
            fixedObservers.add(this);
        }
    }

    public void stopWatching() {
        synchronized (sObserverLists) {
            Set<FixedFileObserver> fixedObservers = sObserverLists.get(mRootPath);
            if ((fixedObservers == null) || (mObserver == null)) return;

            fixedObservers.remove(this);
            if (fixedObservers.size() == 0) mObserver.stopWatching();

            mObserver = null;
        }
    }

    protected void finalize() {stopWatching();}
}
Infirmity answered 25/9, 2015 at 22:52 Comment(3)
Omg thanks for sharing your solution! I almost pulled my hair out after being hit by this bug. I tried it and it works for my case too :)Gripe
Great workaround fix, hopefully this doesn't break when Google does fix in some future release.Dibri
It shouldn't break when there is a fix - just add a bit of overhead. However, Google hasn't fixed this in 3.5 years, so wouldn't hold my breath...Infirmity
C
0

@Fimagena's answer works perfectly for me. For those that have moved on to Kotlin and found the version generated by the Java->Kotlin code converter to be non-functional, here is a working Kotlin version:

package <your package>

import android.os.FileObserver
import java.io.File

var sObserverLists = mutableMapOf<File, MutableSet<FixedFileObserver>>()

abstract class FixedFileObserver(
        path: String,
        private val eventMask: Int = FileObserver.ALL_EVENTS
) {
    private var fileObserver: FileObserver? = null
    private val rootPath: File = File(path)

    abstract fun onEvent(event: Int, relativePath: String?)

    fun startWatching() {
        synchronized(sObserverLists) {
            if (!sObserverLists.containsKey(rootPath)) {
                sObserverLists[rootPath] = mutableSetOf()
            }

            val fixedObservers = sObserverLists[rootPath]

            fileObserver = if (fixedObservers!!.isNotEmpty()) {
                fixedObservers.iterator().next().fileObserver
            } else {
                object : FileObserver(rootPath.path) {
                    override fun onEvent(event: Int, path: String?) {
                        for (fixedObserver in fixedObservers) {
                            if (event and fixedObserver.eventMask != 0) {
                                fixedObserver.onEvent(event, path)
                            }
                        }
                    }
                }
            }
            fixedObservers.add(this)
            fileObserver!!.startWatching()
        }
    }

    fun stopWatching() {
        synchronized(sObserverLists) {
            val fixedObservers = sObserverLists[rootPath]
            if (fixedObservers == null || fileObserver == null) {
                return
            }

            fixedObservers.remove(this)
            if (fixedObservers.isEmpty()) {
                fileObserver!!.stopWatching()
            }

            fileObserver = null
        }
    }
}

And a bonus wrapper class for the rxJava/rxKotlin enthusiasts:

class RxFileObserver(
        private val path: String, eventMask: 
        Int = FileObserver.ALL_EVENTS
) : FixedFileObserver(path, eventMask) {

    private val subject = PublishSubject.create<String>().toSerialized()

    val observable: Observable<String> =
            subject.doOnSubscribe { startWatching() }
                    .doOnDispose { stopWatching() }
                    .share()

    override fun onEvent(event: Int, relativePath: String?) {
        subject.onNext(path)
    }
}
Celestyn answered 13/8, 2018 at 6:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.