Android: Convert GameFileCacheManager to Kotlin

This commit is contained in:
Simonx22 2026-04-06 20:05:13 -04:00
parent 5abf0e3556
commit 301ca5fcb8
2 changed files with 251 additions and 286 deletions

View File

@ -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<GameFile[]> 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<Boolean> sLoadInProgress = new MutableLiveData<>(false);
private static final MutableLiveData<Boolean> sRescanInProgress = new MutableLiveData<>(false);
private GameFileCacheManager()
{
}
public static LiveData<GameFile[]> getGameFiles()
{
return sGameFiles;
}
public static List<GameFile> getGameFilesForPlatformTab(PlatformTab platformTab)
{
GameFile[] allGames = sGameFiles.getValue();
ArrayList<GameFile> 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<Boolean> isLoading()
{
return sLoadInProgress;
}
/**
* Returns true if in the process of rescanning.
*/
public static LiveData<Boolean> 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();
}
}));
}
}

View File

@ -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<GameFile>())
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<Array<GameFile>> {
return gameFiles
}
@JvmStatic
fun getGameFilesForPlatformTab(platformTab: PlatformTab): List<GameFile> {
val allGames = gameFiles.value!!
val platformTabGames = ArrayList<GameFile>()
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<String> {
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<Boolean> {
return loadInProgress
}
/**
* Returns true if in the process of rescanning.
*/
@JvmStatic
fun isRescanning(): LiveData<Boolean> {
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()
}
}
}
}
}