XSharedPreferences: implemented on-demand file watcher

Further optimization of f8aa9d0
File watcher is initiated and kept alive only while there are valid watch keys present.
This commit is contained in:
C3C0 2021-01-28 18:47:18 +01:00
parent 346ef57460
commit c60f9ed9ef
1 changed files with 87 additions and 67 deletions

View File

@ -18,6 +18,7 @@ import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.AccessDeniedException; import java.nio.file.AccessDeniedException;
import java.nio.file.ClosedWatchServiceException;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardWatchEventKinds; import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent; import java.nio.file.WatchEvent;
@ -38,9 +39,9 @@ import de.robv.android.xposed.services.FileResult;
*/ */
public final class XSharedPreferences implements SharedPreferences { public final class XSharedPreferences implements SharedPreferences {
private static final String TAG = "XSharedPreferences"; private static final String TAG = "XSharedPreferences";
private static final HashMap<Path, PrefsData> sInstances = new HashMap<>(); private static final HashMap<WatchKey, PrefsData> sWatcherKeyInstances = new HashMap<>();
private static final Object sContent = new Object(); private static final Object sContent = new Object();
private static Thread sDaemon = null; private static Thread sWatcherDaemon = null;
private static WatchService sWatcher; private static WatchService sWatcher;
private final HashMap<OnSharedPreferenceChangeListener, Object> mListeners = new HashMap<>(); private final HashMap<OnSharedPreferenceChangeListener, Object> mListeners = new HashMap<>();
@ -50,34 +51,21 @@ public final class XSharedPreferences implements SharedPreferences {
private boolean mLoaded = false; private boolean mLoaded = false;
private long mLastModified; private long mLastModified;
private long mFileSize; private long mFileSize;
private boolean mWatcherEnabled; private WatchKey mWatchKey;
private static synchronized WatchService getWatcher() {
if (sWatcher == null) {
try {
sWatcher = new File(XposedInit.prefsBasePath).toPath().getFileSystem().newWatchService();
if (BuildConfig.DEBUG) Log.d(TAG, "Created WatchService instance");
} catch (IOException e) {
Log.e(TAG, "Failed to create WatchService", e);
}
}
if (sWatcher != null && (sDaemon == null || !sDaemon.isAlive())) {
initWatcherDaemon();
}
return sWatcher;
}
private static void initWatcherDaemon() { private static void initWatcherDaemon() {
sDaemon = new Thread() { sWatcherDaemon = new Thread() {
@Override @Override
public void run() { public void run() {
Log.d(TAG, "Watcher daemon thread started"); if (BuildConfig.DEBUG) Log.d(TAG, "Watcher daemon thread started");
while (true) { while (true) {
WatchKey key; WatchKey key;
try { try {
key = sWatcher.take(); key = sWatcher.take();
} catch (ClosedWatchServiceException ignored) {
if (BuildConfig.DEBUG) Log.d(TAG, "Watcher daemon thread finished");
sWatcher = null;
return;
} catch (InterruptedException ignored) { } catch (InterruptedException ignored) {
return; return;
} }
@ -94,14 +82,11 @@ public final class XSharedPreferences implements SharedPreferences {
if (pathStr.endsWith(".bak")) { if (pathStr.endsWith(".bak")) {
if (kind != StandardWatchEventKinds.ENTRY_DELETE) { if (kind != StandardWatchEventKinds.ENTRY_DELETE) {
continue; continue;
} else {
pathStr = path.getFileName().toString();
path = dir.resolve(pathStr.substring(0, pathStr.length() - 4));
} }
} else if (SELinuxHelper.getAppDataFileService().checkFileExists(pathStr + ".bak")) { } else if (SELinuxHelper.getAppDataFileService().checkFileExists(pathStr + ".bak")) {
continue; continue;
} }
PrefsData data = sInstances.get(path); PrefsData data = sWatcherKeyInstances.get(key);
if (data != null && data.hasChanged()) { if (data != null && data.hasChanged()) {
for (OnSharedPreferenceChangeListener l : data.mPrefs.mListeners.keySet()) { for (OnSharedPreferenceChangeListener l : data.mPrefs.mListeners.keySet()) {
try { try {
@ -116,22 +101,9 @@ public final class XSharedPreferences implements SharedPreferences {
} }
} }
}; };
sDaemon.setName(TAG + "-Daemon"); sWatcherDaemon.setName(TAG + "-Daemon");
sDaemon.setDaemon(true); sWatcherDaemon.setDaemon(true);
sDaemon.start(); sWatcherDaemon.start();
}
/**
* Read settings from the specified file.
*
* @param prefFile The file to read the preferences from.
* @param enableWatcher Whether to enable support for preference change listeners
*/
public XSharedPreferences(File prefFile, boolean enableWatcher) {
mFile = prefFile;
mFilename = prefFile.getAbsolutePath();
mWatcherEnabled = enableWatcher;
init();
} }
/** /**
@ -140,7 +112,9 @@ public final class XSharedPreferences implements SharedPreferences {
* @param prefFile The file to read the preferences from. * @param prefFile The file to read the preferences from.
*/ */
public XSharedPreferences(File prefFile) { public XSharedPreferences(File prefFile) {
this(prefFile, false); mFile = prefFile;
mFilename = prefFile.getAbsolutePath();
init();
} }
/** /**
@ -179,7 +153,6 @@ public final class XSharedPreferences implements SharedPreferences {
xposedminversion = MetaDataReader.extractIntPart((String) minVersionRaw); xposedminversion = MetaDataReader.extractIntPart((String) minVersionRaw);
} }
xposedsharedprefs = metaData.containsKey("xposedsharedprefs"); xposedsharedprefs = metaData.containsKey("xposedsharedprefs");
mWatcherEnabled = metaData.containsKey("xposedsharedprefswatcher");
} }
} catch (NumberFormatException | IOException e) { } catch (NumberFormatException | IOException e) {
Log.w(TAG, "Apk parser fails: " + e); Log.w(TAG, "Apk parser fails: " + e);
@ -197,26 +170,52 @@ public final class XSharedPreferences implements SharedPreferences {
} }
private void tryRegisterWatcher() { private void tryRegisterWatcher() {
if (!mWatcherEnabled) { if (mWatchKey != null && mWatchKey.isValid()) {
return; return;
} }
Path path = mFile.toPath();
if (sInstances.containsKey(path)) { synchronized (sWatcherKeyInstances) {
return; Path path = mFile.toPath();
try {
if (sWatcher == null) {
sWatcher = new File(XposedInit.prefsBasePath).toPath().getFileSystem().newWatchService();
if (BuildConfig.DEBUG) Log.d(TAG, "Created WatchService instance");
}
mWatchKey = path.getParent().register(sWatcher, StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);
sWatcherKeyInstances.put(mWatchKey, new PrefsData(this));
if (sWatcherDaemon == null || !sWatcherDaemon.isAlive()) {
initWatcherDaemon();
}
if (BuildConfig.DEBUG) Log.d(TAG, "tryRegisterWatcher: registered file watcher for " + path);
} catch (AccessDeniedException accDeniedEx) {
if (BuildConfig.DEBUG) Log.e(TAG, "tryRegisterWatcher: access denied to " + path);
} catch (Exception e) {
Log.e(TAG, "tryRegisterWatcher: failed to register file watcher", e);
}
} }
try { }
path.getParent().register(getWatcher(), StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);
sInstances.put(path, new PrefsData(this)); private void tryUnregisterWatcher() {
if (BuildConfig.DEBUG) Log.d(TAG, "tryRegisterWatcher: registered file watcher for " + path); synchronized (sWatcherKeyInstances) {
} catch (AccessDeniedException accDeniedEx) { if (mWatchKey != null) {
if (BuildConfig.DEBUG) Log.d(TAG, "tryRegisterWatcher: access denied to " + path); sWatcherKeyInstances.remove(mWatchKey);
} catch (Exception e) { mWatchKey.cancel();
Log.d(TAG, "tryRegisterWatcher: failed to register file watcher", e); mWatchKey = null;
}
boolean atLeastOneValid = false;
for (WatchKey key : sWatcherKeyInstances.keySet()) {
atLeastOneValid |= key.isValid();
}
if (!atLeastOneValid) {
try {
sWatcher.close();
} catch (Exception ignore) { }
}
} }
} }
private void init() { private void init() {
tryRegisterWatcher();
startLoadFromDisk(); startLoadFromDisk();
} }
@ -266,7 +265,15 @@ public final class XSharedPreferences implements SharedPreferences {
if (!mFile.setReadable(true, false)) if (!mFile.setReadable(true, false))
return false; return false;
tryRegisterWatcher(); // Watcher service needs read access to parent directory (looks like execute is not enough)
if (mFile.getParentFile() != null) {
mFile.getParentFile().setReadable(true, false);
}
if (!mListeners.isEmpty()) {
tryRegisterWatcher();
}
return true; return true;
} }
@ -480,25 +487,38 @@ public final class XSharedPreferences implements SharedPreferences {
throw new UnsupportedOperationException("read-only implementation"); throw new UnsupportedOperationException("read-only implementation");
} }
@Deprecated /**
* Registers a callback to be invoked when a change happens to a preference file.<br>
* Note that it is not possible to determine which preference changed exactly and thus
* preference key in callback invocation will always be null.
*
* @param listener The callback that will run.
* @see #unregisterOnSharedPreferenceChangeListener
*/
@Override @Override
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
if (!mWatcherEnabled) if (listener == null)
throw new UnsupportedOperationException("File watcher feature is disabled for this instance"); throw new IllegalArgumentException("listener cannot be null");
synchronized(this) { synchronized(this) {
mListeners.put(listener, sContent); if (mListeners.put(listener, sContent) == null) {
tryRegisterWatcher();
}
} }
} }
@Deprecated /**
* Unregisters a previous callback.
*
* @param listener The callback that should be unregistered.
* @see #registerOnSharedPreferenceChangeListener
*/
@Override @Override
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
if (!mWatcherEnabled)
throw new UnsupportedOperationException("File watcher feature is disabled for this instance");
synchronized(this) { synchronized(this) {
mListeners.remove(listener); if (mListeners.remove(listener) != null && mListeners.isEmpty()) {
tryUnregisterWatcher();
}
} }
} }