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:
parent
346ef57460
commit
c60f9ed9ef
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue