New XSharedPreferences
This commit is contained in:
parent
4672edc4ad
commit
70f967944c
|
|
@ -8,8 +8,6 @@ import android.content.pm.ApplicationInfo;
|
|||
import android.content.pm.PackageManager;
|
||||
import android.content.res.CompatibilityInfo;
|
||||
import android.content.res.XResources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.UserHandle;
|
||||
|
||||
import com.elderdrivers.riru.edxp.config.ConfigManager;
|
||||
import com.elderdrivers.riru.edxp.util.Hookers;
|
||||
|
|
@ -18,7 +16,6 @@ import com.elderdrivers.riru.edxp.util.Utils;
|
|||
import java.io.File;
|
||||
|
||||
import de.robv.android.xposed.XC_MethodHook;
|
||||
import de.robv.android.xposed.XC_MethodReplacement;
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
import de.robv.android.xposed.XposedHelpers;
|
||||
import de.robv.android.xposed.XposedInit;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.elderdrivers.riru.edxp.config.ConfigManager;
|
|||
import com.elderdrivers.riru.edxp.deopt.PrebuiltMethodsDeopter;
|
||||
|
||||
import de.robv.android.xposed.SELinuxHelper;
|
||||
import de.robv.android.xposed.XposedInit;
|
||||
|
||||
import static com.elderdrivers.riru.edxp.util.FileUtils.getDataPathPrefix;
|
||||
|
||||
|
|
@ -55,6 +56,7 @@ public class NormalProxy extends BaseProxy {
|
|||
private void forkPostCommon(int pid, boolean isSystem, String appDataDir, String niceName) {
|
||||
ConfigManager.appDataDir = appDataDir;
|
||||
ConfigManager.niceName = niceName;
|
||||
XposedInit.prefsBasePath = ConfigManager.getPrefsPath("");
|
||||
mRouter.prepare(isSystem);
|
||||
mRouter.onEnterChildProcess();
|
||||
mRouter.loadModulesSafely(true);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
package de.robv.android.xposed;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.ActivityThread;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Environment;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
|
|
@ -28,270 +31,307 @@ import de.robv.android.xposed.services.FileResult;
|
|||
* @deprecated in Android Pie or later was lost by Google, will not remove
|
||||
*/
|
||||
public final class XSharedPreferences implements SharedPreferences {
|
||||
private static final String TAG = "XSharedPreferences";
|
||||
private final File mFile;
|
||||
private final String mFilename;
|
||||
private Map<String, Object> mMap;
|
||||
private boolean mLoaded = false;
|
||||
private long mLastModified;
|
||||
private long mFileSize;
|
||||
private static final String TAG = "XSharedPreferences";
|
||||
private final File mFile;
|
||||
private final String mFilename;
|
||||
private Map<String, Object> mMap;
|
||||
private boolean mLoaded = false;
|
||||
private long mLastModified;
|
||||
private long mFileSize;
|
||||
|
||||
/**
|
||||
* Read settings from the specified file.
|
||||
* @param prefFile The file to read the preferences from.
|
||||
*/
|
||||
public XSharedPreferences(File prefFile) {
|
||||
mFile = prefFile;
|
||||
mFilename = mFile.getAbsolutePath();
|
||||
startLoadFromDisk();
|
||||
}
|
||||
/**
|
||||
* Read settings from the specified file.
|
||||
*
|
||||
* @param prefFile The file to read the preferences from.
|
||||
*/
|
||||
public XSharedPreferences(File prefFile) {
|
||||
mFile = prefFile;
|
||||
mFilename = mFile.getAbsolutePath();
|
||||
startLoadFromDisk();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read settings from the default preferences for a package.
|
||||
* These preferences are returned by {@link PreferenceManager#getDefaultSharedPreferences}.
|
||||
* @param packageName The package name.
|
||||
*/
|
||||
public XSharedPreferences(String packageName) {
|
||||
this(packageName, packageName + "_preferences");
|
||||
}
|
||||
/**
|
||||
* Read settings from the default preferences for a package.
|
||||
* These preferences are returned by {@link PreferenceManager#getDefaultSharedPreferences}.
|
||||
*
|
||||
* @param packageName The package name.
|
||||
*/
|
||||
public XSharedPreferences(String packageName) {
|
||||
this(packageName, packageName + "_preferences");
|
||||
}
|
||||
|
||||
/**
|
||||
* Read settings from a custom preferences file for a package.
|
||||
* These preferences are returned by {@link Context#getSharedPreferences(String, int)}.
|
||||
* @param packageName The package name.
|
||||
* @param prefFileName The file name without ".xml".
|
||||
*/
|
||||
public XSharedPreferences(String packageName, String prefFileName) {
|
||||
mFile = new File(Environment.getDataDirectory(), "data/" + packageName + "/shared_prefs/" + prefFileName + ".xml");
|
||||
mFilename = mFile.getAbsolutePath();
|
||||
startLoadFromDisk();
|
||||
}
|
||||
/**
|
||||
* Read settings from a custom preferences file for a package.
|
||||
* These preferences are returned by {@link Context#getSharedPreferences(String, int)}.
|
||||
*
|
||||
* @param packageName The package name.
|
||||
* @param prefFileName The file name without ".xml".
|
||||
*/
|
||||
public XSharedPreferences(String packageName, String prefFileName) {
|
||||
boolean newModule = false;
|
||||
Set<String> modules = XposedInit.getLoadedModules();
|
||||
for (String m : modules) {
|
||||
if (m.contains("/" + packageName + "-")) {
|
||||
PackageInfo packageInfo = ((Context) ActivityThread.currentActivityThread().getSystemContext()).getPackageManager().getPackageArchiveInfo(m, PackageManager.GET_META_DATA);
|
||||
newModule = packageInfo != null && packageInfo.applicationInfo != null && packageInfo.applicationInfo.metaData.getBoolean("xposedmodule") && packageInfo.applicationInfo.metaData.getInt("xposedminversion", -1) > 92;
|
||||
}
|
||||
}
|
||||
if (newModule && XposedInit.prefsBasePath != null) {
|
||||
mFile = new File(XposedInit.prefsBasePath, packageName + "/" + prefFileName + ".xml");
|
||||
} else {
|
||||
mFile = new File(Environment.getDataDirectory(), "data/" + packageName + "/shared_prefs/" + prefFileName + ".xml");
|
||||
}
|
||||
mFilename = mFile.getAbsolutePath();
|
||||
startLoadFromDisk();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to make the preferences file world-readable.
|
||||
*
|
||||
* <p><strong>Warning:</strong> This is only meant to work around permission "fix" functions that are part
|
||||
* of some recoveries. It doesn't replace the need to open preferences with {@code MODE_WORLD_READABLE}
|
||||
* in the module's UI code. Otherwise, Android will set stricter permissions again during the next save.
|
||||
*
|
||||
* <p>This will only work if executed as root (e.g. {@code initZygote()}) and only if SELinux is disabled.
|
||||
*
|
||||
* @return {@code true} in case the file could be made world-readable.
|
||||
*/
|
||||
@SuppressLint("SetWorldReadable")
|
||||
public boolean makeWorldReadable() {
|
||||
if (!SELinuxHelper.getAppDataFileService().hasDirectFileAccess())
|
||||
return false; // It doesn't make much sense to make the file readable if we wouldn't be able to access it anyway.
|
||||
/**
|
||||
* Tries to make the preferences file world-readable.
|
||||
*
|
||||
* <p><strong>Warning:</strong> This is only meant to work around permission "fix" functions that are part
|
||||
* of some recoveries. It doesn't replace the need to open preferences with {@code MODE_WORLD_READABLE}
|
||||
* in the module's UI code. Otherwise, Android will set stricter permissions again during the next save.
|
||||
*
|
||||
* <p>This will only work if executed as root (e.g. {@code initZygote()}) and only if SELinux is disabled.
|
||||
*
|
||||
* @return {@code true} in case the file could be made world-readable.
|
||||
*/
|
||||
@SuppressLint("SetWorldReadable")
|
||||
public boolean makeWorldReadable() {
|
||||
if (!SELinuxHelper.getAppDataFileService().hasDirectFileAccess())
|
||||
return false; // It doesn't make much sense to make the file readable if we wouldn't be able to access it anyway.
|
||||
|
||||
if (!mFile.exists()) // Just in case - the file should never be created if it doesn't exist.
|
||||
return false;
|
||||
if (!mFile.exists()) // Just in case - the file should never be created if it doesn't exist.
|
||||
return false;
|
||||
|
||||
return mFile.setReadable(true, false);
|
||||
}
|
||||
return mFile.setReadable(true, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file that is backing these preferences.
|
||||
*
|
||||
* <p><strong>Warning:</strong> The file might not be accessible directly.
|
||||
*/
|
||||
public File getFile() {
|
||||
return mFile;
|
||||
}
|
||||
/**
|
||||
* Returns the file that is backing these preferences.
|
||||
*
|
||||
* <p><strong>Warning:</strong> The file might not be accessible directly.
|
||||
*/
|
||||
public File getFile() {
|
||||
return mFile;
|
||||
}
|
||||
|
||||
private void startLoadFromDisk() {
|
||||
synchronized (this) {
|
||||
mLoaded = false;
|
||||
}
|
||||
new Thread("XSharedPreferences-load") {
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (XSharedPreferences.this) {
|
||||
loadFromDiskLocked();
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
private void startLoadFromDisk() {
|
||||
synchronized (this) {
|
||||
mLoaded = false;
|
||||
}
|
||||
new Thread("XSharedPreferences-load") {
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (XSharedPreferences.this) {
|
||||
loadFromDiskLocked();
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
private void loadFromDiskLocked() {
|
||||
if (mLoaded) {
|
||||
return;
|
||||
}
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private void loadFromDiskLocked() {
|
||||
if (mLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map map = null;
|
||||
FileResult result = null;
|
||||
try {
|
||||
result = SELinuxHelper.getAppDataFileService().getFileInputStream(mFilename, mFileSize, mLastModified);
|
||||
if (result.stream != null) {
|
||||
map = XmlUtils.readMapXml(result.stream);
|
||||
result.stream.close();
|
||||
} else {
|
||||
// The file is unchanged, keep the current values
|
||||
map = mMap;
|
||||
}
|
||||
} catch (XmlPullParserException e) {
|
||||
Log.w(TAG, "getSharedPreferences failed for: " + mFilename, e);
|
||||
} catch (FileNotFoundException ignored) {
|
||||
// SharedPreferencesImpl has a canRead() check, so it doesn't log anything in case the file doesn't exist
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "getSharedPreferences failed for: " + mFilename, e);
|
||||
} finally {
|
||||
if (result != null && result.stream != null) {
|
||||
try {
|
||||
result.stream.close();
|
||||
} catch (RuntimeException rethrown) {
|
||||
throw rethrown;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Map map = null;
|
||||
FileResult result = null;
|
||||
try {
|
||||
result = SELinuxHelper.getAppDataFileService().getFileInputStream(mFilename, mFileSize, mLastModified);
|
||||
if (result.stream != null) {
|
||||
map = XmlUtils.readMapXml(result.stream);
|
||||
result.stream.close();
|
||||
} else {
|
||||
// The file is unchanged, keep the current values
|
||||
map = mMap;
|
||||
}
|
||||
} catch (XmlPullParserException e) {
|
||||
Log.w(TAG, "getSharedPreferences failed for: " + mFilename, e);
|
||||
} catch (FileNotFoundException ignored) {
|
||||
// SharedPreferencesImpl has a canRead() check, so it doesn't log anything in case the file doesn't exist
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "getSharedPreferences failed for: " + mFilename, e);
|
||||
} finally {
|
||||
if (result != null && result.stream != null) {
|
||||
try {
|
||||
result.stream.close();
|
||||
} catch (RuntimeException rethrown) {
|
||||
throw rethrown;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mLoaded = true;
|
||||
if (map != null) {
|
||||
mMap = map;
|
||||
mLastModified = result.mtime;
|
||||
mFileSize = result.size;
|
||||
} else {
|
||||
mMap = new HashMap<>();
|
||||
}
|
||||
notifyAll();
|
||||
}
|
||||
mLoaded = true;
|
||||
if (map != null) {
|
||||
mMap = map;
|
||||
mLastModified = result.mtime;
|
||||
mFileSize = result.size;
|
||||
} else {
|
||||
mMap = new HashMap<>();
|
||||
}
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the settings from file if they have changed.
|
||||
*
|
||||
* <p><strong>Warning:</strong> With enforcing SELinux, this call might be quite expensive.
|
||||
*/
|
||||
public synchronized void reload() {
|
||||
if (hasFileChanged())
|
||||
startLoadFromDisk();
|
||||
}
|
||||
/**
|
||||
* Reload the settings from file if they have changed.
|
||||
*
|
||||
* <p><strong>Warning:</strong> With enforcing SELinux, this call might be quite expensive.
|
||||
*/
|
||||
public synchronized void reload() {
|
||||
if (hasFileChanged())
|
||||
startLoadFromDisk();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the file has changed since the last time it has been loaded.
|
||||
*
|
||||
* <p><strong>Warning:</strong> With enforcing SELinux, this call might be quite expensive.
|
||||
*/
|
||||
public synchronized boolean hasFileChanged() {
|
||||
try {
|
||||
FileResult result = SELinuxHelper.getAppDataFileService().statFile(mFilename);
|
||||
return mLastModified != result.mtime || mFileSize != result.size;
|
||||
} catch (FileNotFoundException ignored) {
|
||||
// SharedPreferencesImpl doesn't log anything in case the file doesn't exist
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "hasFileChanged", e);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Check whether the file has changed since the last time it has been loaded.
|
||||
*
|
||||
* <p><strong>Warning:</strong> With enforcing SELinux, this call might be quite expensive.
|
||||
*/
|
||||
public synchronized boolean hasFileChanged() {
|
||||
try {
|
||||
FileResult result = SELinuxHelper.getAppDataFileService().statFile(mFilename);
|
||||
return mLastModified != result.mtime || mFileSize != result.size;
|
||||
} catch (FileNotFoundException ignored) {
|
||||
// SharedPreferencesImpl doesn't log anything in case the file doesn't exist
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "hasFileChanged", e);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void awaitLoadedLocked() {
|
||||
while (!mLoaded) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException unused) {
|
||||
}
|
||||
}
|
||||
}
|
||||
private void awaitLoadedLocked() {
|
||||
while (!mLoaded) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException unused) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@Override
|
||||
public Map<String, ?> getAll() {
|
||||
synchronized (this) {
|
||||
awaitLoadedLocked();
|
||||
return new HashMap<>(mMap);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@Override
|
||||
public Map<String, ?> getAll() {
|
||||
synchronized (this) {
|
||||
awaitLoadedLocked();
|
||||
return new HashMap<>(mMap);
|
||||
}
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@Override
|
||||
public String getString(String key, String defValue) {
|
||||
synchronized (this) {
|
||||
awaitLoadedLocked();
|
||||
String v = (String)mMap.get(key);
|
||||
return v != null ? v : defValue;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@Override
|
||||
public String getString(String key, String defValue) {
|
||||
synchronized (this) {
|
||||
awaitLoadedLocked();
|
||||
String v = (String) mMap.get(key);
|
||||
return v != null ? v : defValue;
|
||||
}
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@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;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@Override
|
||||
public int getInt(String key, int defValue) {
|
||||
synchronized (this) {
|
||||
awaitLoadedLocked();
|
||||
Integer v = (Integer)mMap.get(key);
|
||||
return v != null ? v : defValue;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@Override
|
||||
public int getInt(String key, int defValue) {
|
||||
synchronized (this) {
|
||||
awaitLoadedLocked();
|
||||
Integer v = (Integer) mMap.get(key);
|
||||
return v != null ? v : defValue;
|
||||
}
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@Override
|
||||
public long getLong(String key, long defValue) {
|
||||
synchronized (this) {
|
||||
awaitLoadedLocked();
|
||||
Long v = (Long)mMap.get(key);
|
||||
return v != null ? v : defValue;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@Override
|
||||
public long getLong(String key, long defValue) {
|
||||
synchronized (this) {
|
||||
awaitLoadedLocked();
|
||||
Long v = (Long) mMap.get(key);
|
||||
return v != null ? v : defValue;
|
||||
}
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@Override
|
||||
public float getFloat(String key, float defValue) {
|
||||
synchronized (this) {
|
||||
awaitLoadedLocked();
|
||||
Float v = (Float)mMap.get(key);
|
||||
return v != null ? v : defValue;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@Override
|
||||
public float getFloat(String key, float defValue) {
|
||||
synchronized (this) {
|
||||
awaitLoadedLocked();
|
||||
Float v = (Float) mMap.get(key);
|
||||
return v != null ? v : defValue;
|
||||
}
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@Override
|
||||
public boolean getBoolean(String key, boolean defValue) {
|
||||
synchronized (this) {
|
||||
awaitLoadedLocked();
|
||||
Boolean v = (Boolean)mMap.get(key);
|
||||
return v != null ? v : defValue;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@Override
|
||||
public boolean getBoolean(String key, boolean defValue) {
|
||||
synchronized (this) {
|
||||
awaitLoadedLocked();
|
||||
Boolean v = (Boolean) mMap.get(key);
|
||||
return v != null ? v : defValue;
|
||||
}
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@Override
|
||||
public boolean contains(String key) {
|
||||
synchronized (this) {
|
||||
awaitLoadedLocked();
|
||||
return mMap.containsKey(key);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@Override
|
||||
public boolean contains(String key) {
|
||||
synchronized (this) {
|
||||
awaitLoadedLocked();
|
||||
return mMap.containsKey(key);
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated Not supported by this implementation. */
|
||||
@Deprecated
|
||||
@Override
|
||||
public Editor edit() {
|
||||
throw new UnsupportedOperationException("read-only implementation");
|
||||
}
|
||||
/**
|
||||
* @deprecated Not supported by this implementation.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public Editor edit() {
|
||||
throw new UnsupportedOperationException("read-only implementation");
|
||||
}
|
||||
|
||||
/** @deprecated Not supported by this implementation. */
|
||||
@Deprecated
|
||||
@Override
|
||||
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
|
||||
throw new UnsupportedOperationException("listeners are not supported in this implementation");
|
||||
}
|
||||
/**
|
||||
* @deprecated Not supported by this implementation.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
|
||||
throw new UnsupportedOperationException("listeners are not supported in this implementation");
|
||||
}
|
||||
|
||||
/** @deprecated Not supported by this implementation. */
|
||||
@Deprecated
|
||||
@Override
|
||||
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
|
||||
throw new UnsupportedOperationException("listeners are not supported in this implementation");
|
||||
}
|
||||
/**
|
||||
* @deprecated Not supported by this implementation.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
|
||||
throw new UnsupportedOperationException("listeners are not supported in this implementation");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ public final class XposedInit {
|
|||
private static final String INSTANT_RUN_CLASS = "com.android.tools.fd.runtime.BootstrapApplication";
|
||||
public static volatile boolean disableResources = false;
|
||||
private static final String[] XRESOURCES_CONFLICTING_PACKAGES = {"com.sygic.aura"};
|
||||
public static String prefsBasePath = null;
|
||||
|
||||
private XposedInit() {
|
||||
}
|
||||
|
|
@ -310,6 +311,12 @@ public final class XposedInit {
|
|||
// @GuardedBy("moduleLoadLock")
|
||||
private static final ArraySet<String> loadedModules = new ArraySet<>();
|
||||
|
||||
public static ArraySet<String> getLoadedModules() {
|
||||
synchronized (moduleLoadLock) {
|
||||
return loadedModules;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean loadModules(boolean callInitZygote) throws IOException {
|
||||
boolean hasLoaded = !modulesLoaded.compareAndSet(false, true);
|
||||
if (hasLoaded) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue