summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/UserDataActivity.kt2
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.java421
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.kt381
3 files changed, 382 insertions, 422 deletions
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<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);
-}
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<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)
+}