From 7ef290635148f9ecf83e7751d60c17ffa0433b33 Mon Sep 17 00:00:00 2001 From: Simonx22 Date: Thu, 30 Apr 2026 16:45:38 -0400 Subject: [PATCH] Android: Convert DirectoryInitialization to Kotlin --- .../dolphinemu/activities/UserDataActivity.kt | 2 +- .../utils/DirectoryInitialization.java | 421 ------------------ .../utils/DirectoryInitialization.kt | 381 ++++++++++++++++ 3 files changed, 382 insertions(+), 422 deletions(-) delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/UserDataActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/UserDataActivity.kt index c111c99adf..bf9b8c714d 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/UserDataActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/UserDataActivity.kt @@ -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) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.java deleted file mode 100644 index 2942bfe29f..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.java +++ /dev/null @@ -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 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 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); -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.kt new file mode 100644 index 0000000000..6d83254f83 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.kt @@ -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 { + 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) +}