package biz.bokhorst.xprivacy;
// Based on:
// https://github.com/rovo89/XposedBridge/blob/master/src/de/robv/android/xposed/XSharedPreferences.java
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import android.content.SharedPreferences;
import android.os.Process;
import android.util.Log;
import com.android.internal.util.XmlUtils;
/**
* This class is basically the same as SharedPreferencesImpl from AOSP, but
* read-only and without listeners support. Instead, it is made to be compatible
* with all ROMs.
*/
public final class SharedPreferencesEx implements SharedPreferences {
private final File mFile;
private final File mBackupFile;
private Map<String, Object> mMap;
private boolean mLoaded = false;
private long mLastModified;
private long mFileSize;
private static int cTryMaxCount = 10;
private static int cTryWaitMs = 50;
public SharedPreferencesEx(File prefFile) {
mFile = prefFile;
mBackupFile = makeBackupFile(prefFile);
startLoadFromDisk();
}
public SharedPreferencesEx(String packageName, String prefFileName) {
mFile = new File(Util.getUserDataDirectory(Process.myUid()) + File.pathSeparator + "shared_prefs"
+ File.pathSeparator + prefFileName + ".xml");
mBackupFile = makeBackupFile(mFile);
startLoadFromDisk();
}
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesEx-load") {
@Override
public void run() {
synchronized (SharedPreferencesEx.this) {
loadFromDiskLocked();
}
}
}.start();
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void loadFromDiskLocked() {
int tries = 0;
while (++tries <= cTryMaxCount && !mLoaded && (mFile.exists() || mBackupFile.exists())) {
// Log retry
if (tries > 1)
Util.log(null, Log.WARN, "Load " + mFile + " try=" + tries + " exists=" + mFile.exists() + " readable="
+ mFile.canRead() + " backup=" + mBackupFile.exists());
// Read file if possible
if (mFile.exists() && mFile.canRead() && !mBackupFile.exists()) {
Map map = null;
long lastModified = mFile.lastModified();
long fileSize = mFile.length();
BufferedInputStream str = null;
try {
str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);
map = XmlUtils.readMapXml(str);
} catch (Throwable ex) {
Util.log(null, Log.WARN, "Error reading " + mFile + ": " + ex);
} finally {
if (str != null) {
try {
str.close();
} catch (RuntimeException rethrown) {
throw rethrown;
} catch (Throwable ex) {
Util.log(null, Log.WARN, "Error closing " + mFile + ": " + ex);
}
}
}
if (map != null) {
mLoaded = true;
mMap = map;
mLastModified = lastModified;
mFileSize = fileSize;
notifyAll();
}
}
// Wait for next try
if (!mLoaded && tries < cTryMaxCount)
try {
Thread.sleep(cTryWaitMs);
} catch (Throwable ex) {
Util.bug(null, ex);
}
}
// File not read
if (!mLoaded) {
if (tries >= cTryMaxCount)
// Not loaded: try to load again on next access
Util.log(null, Log.ERROR, "Not loaded " + mFile);
else
mLoaded = true;
mMap = new HashMap<String, Object>();
notifyAll();
}
}
private static File makeBackupFile(File prefsFile) {
return new File(prefsFile.getPath() + ".bak");
}
/**
* Reload the settings from file if they have changed.
*/
public void reload() {
synchronized (this) {
if (hasFileChanged())
startLoadFromDisk();
}
}
private boolean hasFileChanged() {
// canRead returns false for non existing files
if (!mFile.canRead() || mBackupFile.exists())
return true;
long lastModified = mFile.lastModified();
long fileSize = mFile.length();
synchronized (this) {
return (mLastModified != lastModified || mFileSize != fileSize);
}
}
private void awaitLoadedLocked() {
while (!mLoaded)
try {
wait();
} catch (InterruptedException unused) {
}
}
@Override
public Map<String, ?> getAll() {
synchronized (this) {
awaitLoadedLocked();
return new HashMap<String, Object>(mMap);
}
}
@Override
public String getString(String key, String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String) mMap.get(key);
return v != null ? v : defValue;
}
}
@Override
@SuppressWarnings("unchecked")
public Set<String> getStringSet(String key, Set<String> defValues) {
synchronized (this) {
awaitLoadedLocked();
Set<String> v = (Set<String>) mMap.get(key);
return v != null ? v : defValues;
}
}
@Override
public int getInt(String key, int defValue) {
synchronized (this) {
awaitLoadedLocked();
Integer v = (Integer) mMap.get(key);
return v != null ? v : defValue;
}
}
@Override
public long getLong(String key, long defValue) {
synchronized (this) {
awaitLoadedLocked();
Long v = (Long) mMap.get(key);
return v != null ? v : defValue;
}
}
@Override
public float getFloat(String key, float defValue) {
synchronized (this) {
awaitLoadedLocked();
Float v = (Float) mMap.get(key);
return v != null ? v : defValue;
}
}
@Override
public boolean getBoolean(String key, boolean defValue) {
synchronized (this) {
awaitLoadedLocked();
Boolean v = (Boolean) mMap.get(key);
return v != null ? v : defValue;
}
}
@Override
public boolean contains(String key) {
synchronized (this) {
awaitLoadedLocked();
return mMap.containsKey(key);
}
}
@Override
public Editor edit() {
throw new UnsupportedOperationException("read-only implementation");
}
@Override
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
throw new UnsupportedOperationException("listeners are not supported in this implementation");
}
@Override
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
throw new UnsupportedOperationException("listeners are not supported in this implementation");
}
}