diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheManager.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheManager.java deleted file mode 100644 index 50d75d8507..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheManager.java +++ /dev/null @@ -1,286 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.services; - -import android.os.Handler; -import android.os.Looper; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; - -import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting; -import org.dolphinemu.dolphinemu.features.settings.model.ConfigChangedCallback; -import org.dolphinemu.dolphinemu.model.GameFile; -import org.dolphinemu.dolphinemu.model.GameFileCache; -import org.dolphinemu.dolphinemu.ui.platform.Platform; -import org.dolphinemu.dolphinemu.ui.platform.PlatformTab; -import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -/** - * Loads game list data on a separate thread. - */ -public final class GameFileCacheManager -{ - private static GameFileCache sGameFileCache = null; - private static final MutableLiveData sGameFiles = - new MutableLiveData<>(new GameFile[]{}); - private static boolean sFirstLoadDone = false; - private static boolean sRunRescanAfterLoad = false; - private static boolean sRecursiveScanEnabled; - - private static final ExecutorService sExecutor = Executors.newFixedThreadPool(1); - private static final MutableLiveData sLoadInProgress = new MutableLiveData<>(false); - private static final MutableLiveData sRescanInProgress = new MutableLiveData<>(false); - - private GameFileCacheManager() - { - } - - public static LiveData getGameFiles() - { - return sGameFiles; - } - - public static List getGameFilesForPlatformTab(PlatformTab platformTab) - { - GameFile[] allGames = sGameFiles.getValue(); - ArrayList platformTabGames = new ArrayList<>(); - for (GameFile game : allGames) - { - if (Platform.fromInt(game.getPlatform()).toPlatformTab() == platformTab) - { - platformTabGames.add(game); - } - } - return platformTabGames; - } - - public static GameFile getGameFileByGameId(String gameId) - { - GameFile[] allGames = sGameFiles.getValue(); - for (GameFile game : allGames) - { - if (game.getGameId().equals(gameId)) - { - return game; - } - } - return null; - } - - public static GameFile findSecondDisc(GameFile game) - { - GameFile matchWithoutRevision = null; - - GameFile[] allGames = sGameFiles.getValue(); - for (GameFile otherGame : allGames) - { - if (game.getGameId().equals(otherGame.getGameId()) && - game.getDiscNumber() != otherGame.getDiscNumber()) - { - if (game.getRevision() == otherGame.getRevision()) - return otherGame; - else - matchWithoutRevision = otherGame; - } - } - - return matchWithoutRevision; - } - - public static String[] findSecondDiscAndGetPaths(GameFile gameFile) - { - GameFile secondFile = findSecondDisc(gameFile); - if (secondFile == null) - return new String[]{gameFile.getPath()}; - else - return new String[]{gameFile.getPath(), secondFile.getPath()}; - } - - /** - * Returns true if in the process of loading the cache for the first time. - */ - public static LiveData isLoading() - { - return sLoadInProgress; - } - - /** - * Returns true if in the process of rescanning. - */ - public static LiveData isRescanning() - { - return sRescanInProgress; - } - - public static boolean isLoadingOrRescanning() - { - return sLoadInProgress.getValue() || sRescanInProgress.getValue(); - } - - /** - * Asynchronously loads the game file cache from disk, without checking - * if the games are still present in the user's configured folders. - * If this has already been called, calling it again has no effect. - */ - public static void startLoad() - { - createGameFileCacheIfNeeded(); - - if (!sLoadInProgress.getValue()) - { - sLoadInProgress.setValue(true); - new AfterDirectoryInitializationRunner().runWithoutLifecycle( - () -> sExecutor.execute(GameFileCacheManager::load)); - } - } - - /** - * Asynchronously scans for games in the user's configured folders, - * updating the game file cache with the results. - * If loading the game file cache hasn't started or hasn't finished, - * the execution of this will be postponed until it finishes. - */ - public static void startRescan() - { - createGameFileCacheIfNeeded(); - - if (!sRescanInProgress.getValue()) - { - sRescanInProgress.setValue(true); - new AfterDirectoryInitializationRunner().runWithoutLifecycle( - () -> sExecutor.execute(GameFileCacheManager::rescan)); - } - } - - public static GameFile addOrGet(String gamePath) - { - // Common case: The game is in the cache, so just grab it from there. (GameFileCache.addOrGet - // actually already checks for this case, but we want to avoid calling it if possible - // because the executor thread may hold a lock on sGameFileCache for extended periods of time.) - GameFile[] allGames = sGameFiles.getValue(); - for (GameFile game : allGames) - { - if (game.getPath().equals(gamePath)) - { - return game; - } - } - - // Unusual case: The game wasn't found in the cache. - // Scan the game and add it to the cache so that we can return it. - createGameFileCacheIfNeeded(); - return sGameFileCache.addOrGet(gamePath); - } - - /** - * Loads the game file cache from disk, without checking if the - * games are still present in the user's configured folders. - * If this has already been called, calling it again has no effect. - */ - private static void load() - { - if (!sFirstLoadDone) - { - sFirstLoadDone = true; - setUpAutomaticRescan(); - sGameFileCache.load(); - if (sGameFileCache.getSize() != 0) - { - updateGameFileArray(); - } - } - - if (sRunRescanAfterLoad) - { - // Without this, there will be a short blip where the loading indicator in the GUI disappears - // because neither sLoadInProgress nor sRescanInProgress is true - sRescanInProgress.postValue(true); - } - - sLoadInProgress.postValue(false); - - if (sRunRescanAfterLoad) - { - sRunRescanAfterLoad = false; - rescan(); - } - } - - /** - * Scans for games in the user's configured folders, - * updating the game file cache with the results. - * If load hasn't been called before this, the execution of this - * will be postponed until after load runs. - */ - private static void rescan() - { - if (!sFirstLoadDone) - { - sRunRescanAfterLoad = true; - } - else - { - String[] gamePaths = GameFileCache.getAllGamePaths(); - - boolean changed = sGameFileCache.update(gamePaths); - if (changed) - { - updateGameFileArray(); - } - - boolean additionalMetadataChanged = sGameFileCache.updateAdditionalMetadata(); - if (additionalMetadataChanged) - { - updateGameFileArray(); - } - - if (changed || additionalMetadataChanged) - { - sGameFileCache.save(); - } - } - - sRescanInProgress.postValue(false); - } - - private static void updateGameFileArray() - { - GameFile[] gameFilesTemp = sGameFileCache.getAllGames(); - Arrays.sort(gameFilesTemp, (lhs, rhs) -> lhs.getTitle().compareToIgnoreCase(rhs.getTitle())); - sGameFiles.postValue(gameFilesTemp); - } - - private static void createGameFileCacheIfNeeded() - { - // Creating the GameFileCache in the static initializer may be unsafe, because GameFileCache - // relies on native code, and the native library isn't loaded right when the app starts. - // We create it here instead. - - if (sGameFileCache == null) - { - sGameFileCache = new GameFileCache(); - } - } - - private static void setUpAutomaticRescan() - { - sRecursiveScanEnabled = BooleanSetting.MAIN_RECURSIVE_ISO_PATHS.getBoolean(); - new ConfigChangedCallback(() -> - new Handler(Looper.getMainLooper()).post(() -> - { - boolean recursiveScanEnabled = BooleanSetting.MAIN_RECURSIVE_ISO_PATHS.getBoolean(); - if (sRecursiveScanEnabled != recursiveScanEnabled) - { - sRecursiveScanEnabled = recursiveScanEnabled; - startRescan(); - } - })); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheManager.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheManager.kt new file mode 100644 index 0000000000..2ba2c246b3 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheManager.kt @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.services + +import android.os.Handler +import android.os.Looper +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting +import org.dolphinemu.dolphinemu.features.settings.model.ConfigChangedCallback +import org.dolphinemu.dolphinemu.model.GameFile +import org.dolphinemu.dolphinemu.model.GameFileCache +import org.dolphinemu.dolphinemu.ui.platform.Platform +import org.dolphinemu.dolphinemu.ui.platform.PlatformTab +import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner +import java.util.Arrays +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +/** + * Loads game list data on a separate thread. + */ +object GameFileCacheManager { + private var gameFileCache: GameFileCache? = null + private val gameFiles = MutableLiveData(emptyArray()) + private var firstLoadDone = false + private var runRescanAfterLoad = false + private var recursiveScanEnabled = false + + private val executor: ExecutorService = Executors.newFixedThreadPool(1) + private val loadInProgress = MutableLiveData(false) + private val rescanInProgress = MutableLiveData(false) + + @JvmStatic + fun getGameFiles(): LiveData> { + return gameFiles + } + + @JvmStatic + fun getGameFilesForPlatformTab(platformTab: PlatformTab): List { + val allGames = gameFiles.value!! + val platformTabGames = ArrayList() + for (game in allGames) { + if (Platform.fromInt(game.getPlatform()).toPlatformTab() == platformTab) { + platformTabGames.add(game) + } + } + return platformTabGames + } + + @JvmStatic + fun getGameFileByGameId(gameId: String): GameFile? { + val allGames = gameFiles.value!! + for (game in allGames) { + if (game.getGameId() == gameId) { + return game + } + } + return null + } + + @JvmStatic + fun findSecondDisc(game: GameFile): GameFile? { + var matchWithoutRevision: GameFile? = null + + val allGames = gameFiles.value!! + for (otherGame in allGames) { + if (game.getGameId() == otherGame.getGameId() && game.getDiscNumber() != otherGame.getDiscNumber()) { + if (game.getRevision() == otherGame.getRevision()) { + return otherGame + } else { + matchWithoutRevision = otherGame + } + } + } + + return matchWithoutRevision + } + + @JvmStatic + fun findSecondDiscAndGetPaths(gameFile: GameFile?): Array { + val nonNullGameFile = gameFile!! + val secondFile = findSecondDisc(nonNullGameFile) + return if (secondFile == null) { + arrayOf(nonNullGameFile.getPath()) + } else { + arrayOf(nonNullGameFile.getPath(), secondFile.getPath()) + } + } + + /** + * Returns true if in the process of loading the cache for the first time. + */ + @JvmStatic + fun isLoading(): LiveData { + return loadInProgress + } + + /** + * Returns true if in the process of rescanning. + */ + @JvmStatic + fun isRescanning(): LiveData { + return rescanInProgress + } + + @JvmStatic + fun isLoadingOrRescanning(): Boolean { + return loadInProgress.value!! || rescanInProgress.value!! + } + + /** + * Asynchronously loads the game file cache from disk, without checking + * if the games are still present in the user's configured folders. + * If this has already been called, calling it again has no effect. + */ + @JvmStatic + fun startLoad() { + createGameFileCacheIfNeeded() + + if (!loadInProgress.value!!) { + loadInProgress.value = true + AfterDirectoryInitializationRunner().runWithoutLifecycle { executor.execute(::load) } + } + } + + /** + * Asynchronously scans for games in the user's configured folders, + * updating the game file cache with the results. + * If loading the game file cache hasn't started or hasn't finished, + * the execution of this will be postponed until it finishes. + */ + @JvmStatic + fun startRescan() { + createGameFileCacheIfNeeded() + + if (!rescanInProgress.value!!) { + rescanInProgress.value = true + AfterDirectoryInitializationRunner().runWithoutLifecycle { executor.execute(::rescan) } + } + } + + @JvmStatic + fun addOrGet(gamePath: String?): GameFile { + val nonNullGamePath = gamePath!! + + // Common case: The game is in the cache, so just grab it from there. (GameFileCache.addOrGet + // actually already checks for this case, but we want to avoid calling it if possible + // because the executor thread may hold a lock on gameFileCache for extended periods of time.) + val allGames = gameFiles.value!! + for (game in allGames) { + if (game.getPath() == nonNullGamePath) { + return game + } + } + + // Unusual case: The game wasn't found in the cache. + // Scan the game and add it to the cache so that we can return it. + createGameFileCacheIfNeeded() + return gameFileCache!!.addOrGet(nonNullGamePath)!! + } + + /** + * Loads the game file cache from disk, without checking if the + * games are still present in the user's configured folders. + * If this has already been called, calling it again has no effect. + */ + private fun load() { + if (!firstLoadDone) { + firstLoadDone = true + setUpAutomaticRescan() + gameFileCache!!.load() + if (gameFileCache!!.getSize() != 0) { + updateGameFileArray() + } + } + + if (runRescanAfterLoad) { + // Without this, there will be a short blip where the loading indicator in the GUI disappears + // because neither loadInProgress nor rescanInProgress is true + rescanInProgress.postValue(true) + } + + loadInProgress.postValue(false) + + if (runRescanAfterLoad) { + runRescanAfterLoad = false + rescan() + } + } + + /** + * Scans for games in the user's configured folders, + * updating the game file cache with the results. + * If load hasn't been called before this, the execution of this + * will be postponed until after load runs. + */ + private fun rescan() { + if (!firstLoadDone) { + runRescanAfterLoad = true + } else { + val gamePaths = GameFileCache.getAllGamePaths() + + val changed = gameFileCache!!.update(gamePaths) + if (changed) { + updateGameFileArray() + } + + val additionalMetadataChanged = gameFileCache!!.updateAdditionalMetadata() + if (additionalMetadataChanged) { + updateGameFileArray() + } + + if (changed || additionalMetadataChanged) { + gameFileCache!!.save() + } + } + + rescanInProgress.postValue(false) + } + + private fun updateGameFileArray() { + val gameFilesTemp = gameFileCache!!.getAllGames() + Arrays.sort(gameFilesTemp) { lhs, rhs -> + lhs.getTitle().compareTo(rhs.getTitle(), ignoreCase = true) + } + gameFiles.postValue(gameFilesTemp) + } + + private fun createGameFileCacheIfNeeded() { + // Creating the GameFileCache in the static initializer may be unsafe, because GameFileCache + // relies on native code, and the native library isn't loaded right when the app starts. + // We create it here instead. + if (gameFileCache == null) { + gameFileCache = GameFileCache() + } + } + + private fun setUpAutomaticRescan() { + recursiveScanEnabled = BooleanSetting.MAIN_RECURSIVE_ISO_PATHS.boolean + ConfigChangedCallback { + Handler(Looper.getMainLooper()).post { + val recursiveScanEnabled = BooleanSetting.MAIN_RECURSIVE_ISO_PATHS.boolean + if (this.recursiveScanEnabled != recursiveScanEnabled) { + this.recursiveScanEnabled = recursiveScanEnabled + startRescan() + } + } + } + } +}