This commit is contained in:
Simonx22 2026-05-08 23:06:35 -04:00 committed by GitHub
commit 2f3c85f733
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 382 additions and 422 deletions

View File

@ -244,7 +244,7 @@ class UserDataActivity : AppCompatActivity(), ThemeProvider {
deleteChildrenRecursively(userDirectory)
DirectoryInitialization.getGameListCache(this).delete()
DirectoryInitialization.getGameListCache().delete()
var ze: ZipEntry? = zis.nextEntry
val buffer = ByteArray(BUFFER_SIZE)

View File

@ -1,421 +0,0 @@
/*
* Copyright 2014 Dolphin Emulator Project
* SPDX-License-Identifier: GPL-2.0-or-later
*/
package org.dolphinemu.dolphinemu.utils;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Environment;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.preference.PreferenceManager;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.dolphinemu.dolphinemu.NativeLibrary;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting;
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting;
/**
* A class that spawns its own thread in order perform initialization.
*
* The initialization steps include:
* - Extracting the Sys directory from the APK so it can be accessed using regular file APIs
* - Letting the native code know where on external storage it should place the User directory
* - Running the native code's init steps (which include things like populating the User directory)
*/
public final class DirectoryInitialization
{
public static final String EXTRA_STATE = "directoryState";
private static final MutableLiveData<DirectoryInitializationState> directoryState =
new MutableLiveData<>(DirectoryInitializationState.NOT_YET_INITIALIZED);
private static volatile boolean areDirectoriesAvailable = false;
private static String userPath;
private static String sysPath;
private static String driverPath;
private static boolean isUsingLegacyUserDirectory = false;
public enum DirectoryInitializationState
{
NOT_YET_INITIALIZED,
INITIALIZING,
DOLPHIN_DIRECTORIES_INITIALIZED
}
public static void start(Context context)
{
if (directoryState.getValue() != DirectoryInitializationState.NOT_YET_INITIALIZED)
return;
directoryState.setValue(DirectoryInitializationState.INITIALIZING);
// Can take a few seconds to run, so don't block UI thread.
new Thread(() -> init(context)).start();
}
private static void init(Context context)
{
if (directoryState.getValue() == DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED)
return;
if (!setDolphinUserDirectory(context))
{
ContextCompat.getMainExecutor(context).execute(() ->
{
Toast.makeText(context, R.string.external_storage_not_mounted, Toast.LENGTH_LONG).show();
System.exit(1);
});
return;
}
extractSysDirectory(context);
NativeLibrary.Initialize();
areDirectoriesAvailable = true;
checkThemeSettings(context);
directoryState.postValue(DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED);
}
@Nullable private static File getLegacyUserDirectoryPath()
{
File externalPath = Environment.getExternalStorageDirectory();
if (externalPath == null)
return null;
return new File(externalPath, "dolphin-emu");
}
@Nullable public static File getUserDirectoryPath(Context context)
{
if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()))
return null;
isUsingLegacyUserDirectory =
preferLegacyUserDirectory(context) && PermissionsHandler.hasWriteAccess(context);
return isUsingLegacyUserDirectory ? getLegacyUserDirectoryPath() :
context.getExternalFilesDir(null);
}
private static boolean setDolphinUserDirectory(Context context)
{
File path = DirectoryInitialization.getUserDirectoryPath(context);
if (path == null)
return false;
userPath = path.getAbsolutePath();
Log.debug("[DirectoryInitialization] User Dir: " + userPath);
NativeLibrary.SetUserDirectory(userPath);
File cacheDir = context.getExternalCacheDir();
if (cacheDir == null)
{
// In some custom ROMs getExternalCacheDir might return null for some reasons. If that is the case, fallback to getCacheDir which seems to work just fine.
cacheDir = context.getCacheDir();
if (cacheDir == null)
return false;
}
Log.debug("[DirectoryInitialization] Cache Dir: " + cacheDir.getPath());
NativeLibrary.SetCacheDirectory(cacheDir.getPath());
return true;
}
private static void extractSysDirectory(Context context)
{
File sysDirectory = new File(context.getFilesDir(), "Sys");
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
String revision = NativeLibrary.GetGitRevision();
if (!preferences.getString("sysDirectoryVersion", "").equals(revision))
{
// There is no extracted Sys directory, or there is a Sys directory from another
// version of Dolphin that might contain outdated files. Let's (re-)extract Sys.
deleteDirectoryRecursively(sysDirectory);
copyAssetFolder("Sys", sysDirectory, context);
SharedPreferences.Editor editor = preferences.edit();
editor.putString("sysDirectoryVersion", revision);
editor.apply();
}
// Let the native code know where the Sys directory is.
sysPath = sysDirectory.getPath();
SetSysDirectory(sysPath);
File driverDirectory = new File(context.getFilesDir(), "GPUDrivers");
driverDirectory.mkdirs();
File driverExtractedDir = new File(driverDirectory, "Extracted");
driverExtractedDir.mkdirs();
File driverTmpDir = new File(driverDirectory, "Tmp");
driverTmpDir.mkdirs();
File driverFileRedirectDir = new File(driverDirectory, "FileRedirect");
driverFileRedirectDir.mkdirs();
SetGpuDriverDirectories(driverDirectory.getPath(),
context.getApplicationInfo().nativeLibraryDir);
DirectoryInitialization.driverPath = driverExtractedDir.getAbsolutePath();
}
private static void deleteDirectoryRecursively(@NonNull final File file)
{
if (file.isDirectory())
{
File[] files = file.listFiles();
if (files == null)
{
return;
}
for (File child : files)
deleteDirectoryRecursively(child);
}
if (!file.delete())
{
Log.error("[DirectoryInitialization] Failed to delete " + file.getAbsolutePath());
}
}
public static boolean shouldStart(Context context)
{
return getDolphinDirectoriesState().getValue() ==
DirectoryInitializationState.NOT_YET_INITIALIZED &&
!isWaitingForWriteAccess(context);
}
public static boolean areDolphinDirectoriesReady()
{
return directoryState.getValue() ==
DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED;
}
public static LiveData<DirectoryInitializationState> getDolphinDirectoriesState()
{
return directoryState;
}
public static String getUserDirectory()
{
if (!areDirectoriesAvailable)
{
throw new IllegalStateException(
"DirectoryInitialization must run before accessing the user directory!");
}
return userPath;
}
public static String getSysDirectory()
{
if (!areDirectoriesAvailable)
{
throw new IllegalStateException(
"DirectoryInitialization must run before accessing the Sys directory!");
}
return sysPath;
}
public static String getExtractedDriverDirectory()
{
if (!areDirectoriesAvailable)
{
throw new IllegalStateException(
"DirectoryInitialization must run before accessing the driver directory!");
}
return driverPath;
}
public static File getGameListCache(Context context)
{
return new File(NativeLibrary.GetCacheDirectory(), "gamelist.cache");
}
private static boolean copyAsset(String asset, File output, Context context)
{
Log.verbose("[DirectoryInitialization] Copying File " + asset + " to " + output);
try
{
try (InputStream in = context.getAssets().open(asset))
{
try (OutputStream out = new FileOutputStream(output))
{
copyFile(in, out);
return true;
}
}
}
catch (IOException e)
{
Log.error("[DirectoryInitialization] Failed to copy asset file: " + asset + e.getMessage());
}
return false;
}
private static void copyAssetFolder(String assetFolder, File outputFolder, Context context)
{
Log.verbose("[DirectoryInitialization] Copying Folder " + assetFolder + " to " + outputFolder);
try
{
String[] assetList = context.getAssets().list(assetFolder);
if (assetList == null)
{
return;
}
boolean createdFolder = false;
for (String file : assetList)
{
if (!createdFolder)
{
if (!outputFolder.mkdir())
{
Log.error("[DirectoryInitialization] Failed to create folder " +
outputFolder.getAbsolutePath());
}
createdFolder = true;
}
copyAssetFolder(assetFolder + File.separator + file, new File(outputFolder, file), context);
copyAsset(assetFolder + File.separator + file, new File(outputFolder, file), context);
}
}
catch (IOException e)
{
Log.error("[DirectoryInitialization] Failed to copy asset folder: " + assetFolder +
e.getMessage());
}
}
private static void copyFile(InputStream in, OutputStream out) throws IOException
{
byte[] buffer = new byte[1024];
int read;
while ((read = in.read(buffer)) != -1)
{
out.write(buffer, 0, read);
}
}
public static boolean preferOldFolderPicker(Context context)
{
// As of January 2021, ACTION_OPEN_DOCUMENT_TREE seems to be broken on the Nvidia Shield TV
// (the activity can't be navigated correctly with a gamepad). We can use the old folder picker
// for the time being - Android 11 hasn't been released for this device. We have an explicit
// check for Android 11 below in hopes that Nvidia will fix this before releasing Android 11.
//
// No Android TV device other than the Nvidia Shield TV is known to have an implementation of
// ACTION_OPEN_DOCUMENT or ACTION_OPEN_DOCUMENT_TREE that even launches, but "fortunately", no
// Android TV device other than the Shield TV is known to be able to run Dolphin (either due to
// the 64-bit requirement or due to the GLES 3.0 requirement), so we can ignore this problem.
//
// All phones which are running a compatible version of Android support ACTION_OPEN_DOCUMENT and
// ACTION_OPEN_DOCUMENT_TREE, as this is required by the mobile Android CTS (unlike Android TV).
return Build.VERSION.SDK_INT < Build.VERSION_CODES.R &&
PermissionsHandler.isExternalStorageLegacy() && TvUtil.isLeanback(context);
}
private static boolean isExternalFilesDirEmpty(Context context)
{
File dir = context.getExternalFilesDir(null);
if (dir == null)
return false; // External storage not available
File[] contents = dir.listFiles();
return contents == null || contents.length == 0;
}
private static boolean legacyUserDirectoryExists()
{
try
{
return getLegacyUserDirectoryPath().exists();
}
catch (SecurityException e)
{
// Most likely we don't have permission to read external storage.
// Return true so that external storage permissions will be requested.
//
// Strangely, we don't seem to trigger this case in practice, even with no permissions...
// But this only makes things more convenient for users, so no harm done.
return true;
}
}
private static boolean preferLegacyUserDirectory(Context context)
{
return PermissionsHandler.isExternalStorageLegacy() &&
!PermissionsHandler.isWritePermissionDenied() && isExternalFilesDirEmpty(context) &&
legacyUserDirectoryExists();
}
public static boolean isUsingLegacyUserDirectory()
{
return isUsingLegacyUserDirectory;
}
public static boolean isWaitingForWriteAccess(Context context)
{
// This first check is only for performance, not correctness
if (directoryState.getValue() != DirectoryInitializationState.NOT_YET_INITIALIZED)
return false;
return preferLegacyUserDirectory(context) && !PermissionsHandler.hasWriteAccess(context);
}
private static void checkThemeSettings(Context context)
{
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
if (IntSetting.MAIN_INTERFACE_THEME.getInt() !=
preferences.getInt(ThemeHelper.CURRENT_THEME, ThemeHelper.DEFAULT))
{
preferences.edit()
.putInt(ThemeHelper.CURRENT_THEME, IntSetting.MAIN_INTERFACE_THEME.getInt())
.apply();
}
if (IntSetting.MAIN_INTERFACE_THEME_MODE.getInt() !=
preferences.getInt(ThemeHelper.CURRENT_THEME_MODE,
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM))
{
preferences.edit()
.putInt(ThemeHelper.CURRENT_THEME_MODE, IntSetting.MAIN_INTERFACE_THEME_MODE.getInt())
.apply();
}
if (BooleanSetting.MAIN_USE_BLACK_BACKGROUNDS.getBoolean() !=
preferences.getBoolean(ThemeHelper.USE_BLACK_BACKGROUNDS, false))
{
preferences.edit()
.putBoolean(ThemeHelper.USE_BLACK_BACKGROUNDS,
BooleanSetting.MAIN_USE_BLACK_BACKGROUNDS.getBoolean())
.apply();
}
}
private static native void SetSysDirectory(String path);
private static native void SetGpuDriverDirectories(String path, String libPath);
}

View File

@ -0,0 +1,381 @@
/*
* Copyright 2014 Dolphin Emulator Project
* SPDX-License-Identifier: GPL-2.0-or-later
*/
package org.dolphinemu.dolphinemu.utils
import android.content.Context
import android.os.Build
import android.os.Environment
import android.widget.Toast
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.preference.PreferenceManager
import org.dolphinemu.dolphinemu.NativeLibrary
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import kotlin.concurrent.thread
import kotlin.system.exitProcess
/**
* A class that spawns its own thread in order perform initialization.
*
* The initialization steps include:
* - Extracting the Sys directory from the APK so it can be accessed using regular file APIs
* - Letting the native code know where on external storage it should place the User directory
* - Running the native code's init steps (which include things like populating the User directory)
*/
object DirectoryInitialization {
private val directoryState = MutableLiveData(DirectoryInitializationState.NOT_YET_INITIALIZED)
@Volatile
private var areDirectoriesAvailable = false
private lateinit var userPath: String
private lateinit var driverPath: String
private var usingLegacyUserDirectory = false
enum class DirectoryInitializationState {
NOT_YET_INITIALIZED, INITIALIZING, DOLPHIN_DIRECTORIES_INITIALIZED
}
@JvmStatic
fun start(context: Context) {
if (directoryState.value != DirectoryInitializationState.NOT_YET_INITIALIZED) {
return
}
directoryState.value = DirectoryInitializationState.INITIALIZING
// Can take a few seconds to run, so don't block UI thread.
thread { init(context) }
}
private fun init(context: Context) {
if (directoryState.value == DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED) {
return
}
if (!setDolphinUserDirectory(context)) {
ContextCompat.getMainExecutor(context).execute {
Toast.makeText(context, R.string.external_storage_not_mounted, Toast.LENGTH_LONG)
.show()
exitProcess(1)
}
return
}
extractSysDirectory(context)
NativeLibrary.Initialize()
areDirectoriesAvailable = true
checkThemeSettings(context)
directoryState.postValue(DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED)
}
private fun getLegacyUserDirectoryPath(): File? {
val externalPath = Environment.getExternalStorageDirectory() ?: return null
return File(externalPath, "dolphin-emu")
}
@JvmStatic
fun getUserDirectoryPath(context: Context?): File? {
if (context == null) {
return null
}
if (Environment.getExternalStorageState() != Environment.MEDIA_MOUNTED) {
return null
}
usingLegacyUserDirectory =
preferLegacyUserDirectory(context) && PermissionsHandler.hasWriteAccess(context)
return if (usingLegacyUserDirectory) {
getLegacyUserDirectoryPath()
} else {
context.getExternalFilesDir(null)
}
}
private fun setDolphinUserDirectory(context: Context): Boolean {
val path = getUserDirectoryPath(context) ?: return false
userPath = path.absolutePath
Log.debug("[DirectoryInitialization] User Dir: $userPath")
NativeLibrary.SetUserDirectory(userPath)
var cacheDir = context.externalCacheDir
if (cacheDir == null) {
// In some custom ROMs getExternalCacheDir might return null for some reason. If that
// is the case, fallback to getCacheDir which seems to work just fine.
cacheDir = context.cacheDir ?: return false
}
Log.debug("[DirectoryInitialization] Cache Dir: ${cacheDir.path}")
NativeLibrary.SetCacheDirectory(cacheDir.path)
return true
}
private fun extractSysDirectory(context: Context) {
val sysDirectory = File(context.filesDir, "Sys")
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
val revision = NativeLibrary.GetGitRevision()
if (preferences.getString("sysDirectoryVersion", "") != revision) {
// There is no extracted Sys directory, or there is a Sys directory from another
// version of Dolphin that might contain outdated files. Let's (re-)extract Sys.
deleteDirectoryRecursively(sysDirectory)
copyAssetFolder("Sys", sysDirectory, context)
preferences.edit {
putString("sysDirectoryVersion", revision)
}
}
// Let the native code know where the Sys directory is.
SetSysDirectory(sysDirectory.path)
val driverDirectory = File(context.filesDir, "GPUDrivers")
driverDirectory.mkdirs()
val driverExtractedDir = File(driverDirectory, "Extracted")
driverExtractedDir.mkdirs()
val driverTmpDir = File(driverDirectory, "Tmp")
driverTmpDir.mkdirs()
val driverFileRedirectDir = File(driverDirectory, "FileRedirect")
driverFileRedirectDir.mkdirs()
SetGpuDriverDirectories(driverDirectory.path, context.applicationInfo.nativeLibraryDir)
driverPath = driverExtractedDir.absolutePath
}
private fun deleteDirectoryRecursively(file: File) {
if (file.isDirectory) {
val files = file.listFiles() ?: return
for (child in files) {
deleteDirectoryRecursively(child)
}
}
if (!file.delete()) {
Log.error("[DirectoryInitialization] Failed to delete ${file.absolutePath}")
}
}
@JvmStatic
fun shouldStart(context: Context): Boolean {
return getDolphinDirectoriesState().value == DirectoryInitializationState.NOT_YET_INITIALIZED && !isWaitingForWriteAccess(
context
)
}
@JvmStatic
fun areDolphinDirectoriesReady(): Boolean {
return directoryState.value == DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED
}
@JvmStatic
fun getDolphinDirectoriesState(): LiveData<DirectoryInitializationState> {
return directoryState
}
@JvmStatic
fun getUserDirectory(): String {
if (!areDirectoriesAvailable) {
throw IllegalStateException(
"DirectoryInitialization must run before accessing the user directory!"
)
}
return userPath
}
@JvmStatic
fun getExtractedDriverDirectory(): String {
if (!areDirectoriesAvailable) {
throw IllegalStateException(
"DirectoryInitialization must run before accessing the driver directory!"
)
}
return driverPath
}
@JvmStatic
fun getGameListCache(): File {
return File(NativeLibrary.GetCacheDirectory(), "gamelist.cache")
}
private fun copyAsset(asset: String, output: File, context: Context) {
Log.verbose("[DirectoryInitialization] Copying File $asset to $output")
try {
context.assets.open(asset).use { input ->
FileOutputStream(output).use { outputStream ->
copyFile(input, outputStream)
}
}
} catch (e: IOException) {
Log.error("[DirectoryInitialization] Failed to copy asset file: $asset${e.message}")
}
}
private fun copyAssetFolder(assetFolder: String, outputFolder: File, context: Context) {
Log.verbose("[DirectoryInitialization] Copying Folder $assetFolder to $outputFolder")
try {
val assetList = context.assets.list(assetFolder) ?: return
var createdFolder = false
for (file in assetList) {
if (!createdFolder) {
if (!outputFolder.mkdir()) {
Log.error(
"[DirectoryInitialization] Failed to create folder " + outputFolder.absolutePath
)
}
createdFolder = true
}
val childAsset = assetFolder + File.separator + file
val childOutput = File(outputFolder, file)
copyAssetFolder(childAsset, childOutput, context)
copyAsset(childAsset, childOutput, context)
}
} catch (e: IOException) {
Log.error(
"[DirectoryInitialization] Failed to copy asset folder: $assetFolder${e.message}"
)
}
}
@Throws(IOException::class)
private fun copyFile(input: InputStream, output: OutputStream) {
val buffer = ByteArray(1024)
var read: Int
while (input.read(buffer).also { read = it } != -1) {
output.write(buffer, 0, read)
}
}
@JvmStatic
fun preferOldFolderPicker(context: Context): Boolean {
// As of January 2021, ACTION_OPEN_DOCUMENT_TREE seems to be broken on the Nvidia Shield TV
// (the activity can't be navigated correctly with a gamepad). We can use the old folder
// picker for the time being - Android 11 hasn't been released for this device. We have an
// explicit check for Android 11 below in hopes that Nvidia will fix this before releasing
// Android 11.
//
// No Android TV device other than the Nvidia Shield TV is known to have an implementation
// of ACTION_OPEN_DOCUMENT or ACTION_OPEN_DOCUMENT_TREE that even launches, but
// "fortunately", no Android TV device other than the Shield TV is known to be able to run
// Dolphin (either due to the 64-bit requirement or due to the GLES 3.0 requirement), so
// we can ignore this problem.
//
// All phones which are running a compatible version of Android support ACTION_OPEN_DOCUMENT
// and ACTION_OPEN_DOCUMENT_TREE, as this is required by the mobile Android CTS (unlike
// Android TV).
return Build.VERSION.SDK_INT < Build.VERSION_CODES.R && PermissionsHandler.isExternalStorageLegacy() && TvUtil.isLeanback(
context
)
}
private fun isExternalFilesDirEmpty(context: Context): Boolean {
val dir =
context.getExternalFilesDir(null) ?: return false // External storage not available
val contents = dir.listFiles()
return contents == null || contents.isEmpty()
}
private fun legacyUserDirectoryExists(): Boolean {
return try {
getLegacyUserDirectoryPath()?.exists() == true
} catch (_: SecurityException) {
// Most likely we don't have permission to read external storage.
// Return true so that external storage permissions will be requested.
//
// Strangely, we don't seem to trigger this case in practice, even with no
// permissions... But this only makes things more convenient for users, so no harm
// done.
true
}
}
private fun preferLegacyUserDirectory(context: Context): Boolean {
return PermissionsHandler.isExternalStorageLegacy() && !PermissionsHandler.isWritePermissionDenied() && isExternalFilesDirEmpty(
context
) && legacyUserDirectoryExists()
}
@JvmStatic
fun isUsingLegacyUserDirectory(): Boolean {
return usingLegacyUserDirectory
}
@JvmStatic
fun isWaitingForWriteAccess(context: Context): Boolean {
// This first check is only for performance, not correctness
if (directoryState.value != DirectoryInitializationState.NOT_YET_INITIALIZED) {
return false
}
return preferLegacyUserDirectory(context) && !PermissionsHandler.hasWriteAccess(context)
}
private fun checkThemeSettings(context: Context) {
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
if (IntSetting.MAIN_INTERFACE_THEME.int != preferences.getInt(
ThemeHelper.CURRENT_THEME, ThemeHelper.DEFAULT
)
) {
preferences.edit {
putInt(ThemeHelper.CURRENT_THEME, IntSetting.MAIN_INTERFACE_THEME.int)
}
}
if (IntSetting.MAIN_INTERFACE_THEME_MODE.int != preferences.getInt(
ThemeHelper.CURRENT_THEME_MODE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
)
) {
preferences.edit {
putInt(ThemeHelper.CURRENT_THEME_MODE, IntSetting.MAIN_INTERFACE_THEME_MODE.int)
}
}
if (BooleanSetting.MAIN_USE_BLACK_BACKGROUNDS.boolean != preferences.getBoolean(
ThemeHelper.USE_BLACK_BACKGROUNDS, false
)
) {
preferences.edit {
putBoolean(
ThemeHelper.USE_BLACK_BACKGROUNDS,
BooleanSetting.MAIN_USE_BLACK_BACKGROUNDS.boolean
)
}
}
}
@Suppress("FunctionName")
@JvmStatic
private external fun SetSysDirectory(path: String)
@Suppress("FunctionName")
@JvmStatic
private external fun SetGpuDriverDirectories(path: String, libPath: String)
}