1 package org.dolphinemu.dolphinemu.services; 2 3 import android.app.IntentService; 4 import android.content.Context; 5 import android.content.Intent; 6 7 import androidx.localbroadcastmanager.content.LocalBroadcastManager; 8 9 import org.dolphinemu.dolphinemu.model.GameFile; 10 import org.dolphinemu.dolphinemu.model.GameFileCache; 11 import org.dolphinemu.dolphinemu.ui.platform.Platform; 12 import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner; 13 14 import java.io.File; 15 import java.util.ArrayList; 16 import java.util.Arrays; 17 import java.util.List; 18 import java.util.concurrent.atomic.AtomicBoolean; 19 import java.util.concurrent.atomic.AtomicReference; 20 21 /** 22 * A service that loads game list data on a separate thread. 23 */ 24 public final class GameFileCacheService extends IntentService 25 { 26 public static final String BROADCAST_ACTION = "org.dolphinemu.dolphinemu.GAME_FILE_CACHE_UPDATED"; 27 28 private static final String ACTION_LOAD = "org.dolphinemu.dolphinemu.LOAD_GAME_FILE_CACHE"; 29 private static final String ACTION_RESCAN = "org.dolphinemu.dolphinemu.RESCAN_GAME_FILE_CACHE"; 30 31 private static GameFileCache gameFileCache = null; 32 private static AtomicReference<GameFile[]> gameFiles = new AtomicReference<>(new GameFile[]{}); 33 private static AtomicBoolean hasLoadedCache = new AtomicBoolean(false); 34 private static AtomicBoolean hasScannedLibrary = new AtomicBoolean(false); 35 GameFileCacheService()36 public GameFileCacheService() 37 { 38 // Superclass constructor is called to name the thread on which this service executes. 39 super("GameFileCacheService"); 40 } 41 getGameFilesForPlatform(Platform platform)42 public static List<GameFile> getGameFilesForPlatform(Platform platform) 43 { 44 GameFile[] allGames = gameFiles.get(); 45 ArrayList<GameFile> platformGames = new ArrayList<>(); 46 for (GameFile game : allGames) 47 { 48 if (Platform.fromNativeInt(game.getPlatform()) == platform) 49 { 50 platformGames.add(game); 51 } 52 } 53 return platformGames; 54 } 55 getGameFileByGameId(String gameId)56 public static GameFile getGameFileByGameId(String gameId) 57 { 58 GameFile[] allGames = gameFiles.get(); 59 for (GameFile game : allGames) 60 { 61 if (game.getGameId().equals(gameId)) 62 { 63 return game; 64 } 65 } 66 return null; 67 } 68 findSecondDisc(GameFile game)69 public static GameFile findSecondDisc(GameFile game) 70 { 71 GameFile matchWithoutRevision = null; 72 73 GameFile[] allGames = gameFiles.get(); 74 for (GameFile otherGame : allGames) 75 { 76 if (game.getGameId().equals(otherGame.getGameId()) && 77 game.getDiscNumber() != otherGame.getDiscNumber()) 78 { 79 if (game.getRevision() == otherGame.getRevision()) 80 return otherGame; 81 else 82 matchWithoutRevision = otherGame; 83 } 84 } 85 86 return matchWithoutRevision; 87 } 88 hasLoadedCache()89 public static boolean hasLoadedCache() 90 { 91 return hasLoadedCache.get(); 92 } 93 hasScannedLibrary()94 public static boolean hasScannedLibrary() 95 { 96 return hasScannedLibrary.get(); 97 } 98 startService(Context context, String action)99 private static void startService(Context context, String action) 100 { 101 Intent intent = new Intent(context, GameFileCacheService.class); 102 intent.setAction(action); 103 context.startService(intent); 104 } 105 106 /** 107 * Asynchronously loads the game file cache from disk without checking 108 * which games are present on the file system. 109 */ startLoad(Context context)110 public static void startLoad(Context context) 111 { 112 new AfterDirectoryInitializationRunner().run(context, false, 113 () -> startService(context, ACTION_LOAD)); 114 } 115 116 /** 117 * Asynchronously scans for games in the user's configured folders, 118 * updating the game file cache with the results. 119 * If startLoad hasn't been called before this, this has no effect. 120 */ startRescan(Context context)121 public static void startRescan(Context context) 122 { 123 new AfterDirectoryInitializationRunner().run(context, false, 124 () -> startService(context, ACTION_RESCAN)); 125 } 126 addOrGet(String gamePath)127 public static GameFile addOrGet(String gamePath) 128 { 129 // The existence of this one function, which is called from one 130 // single place, forces us to use synchronization in onHandleIntent... 131 // A bit annoying, but should be good enough for now 132 synchronized (gameFileCache) 133 { 134 return gameFileCache.addOrGet(gamePath); 135 } 136 } 137 138 @Override onHandleIntent(Intent intent)139 protected void onHandleIntent(Intent intent) 140 { 141 // Load the game list cache if it isn't already loaded, otherwise do nothing 142 if (ACTION_LOAD.equals(intent.getAction()) && gameFileCache == null) 143 { 144 GameFileCache temp = new GameFileCache(getCacheDir() + File.separator + "gamelist.cache"); 145 synchronized (temp) 146 { 147 gameFileCache = temp; 148 gameFileCache.load(); 149 updateGameFileArray(); 150 hasLoadedCache.set(true); 151 sendBroadcast(); 152 } 153 } 154 155 // Rescan the file system and update the game list cache with the results 156 if (ACTION_RESCAN.equals(intent.getAction()) && gameFileCache != null) 157 { 158 synchronized (gameFileCache) 159 { 160 boolean changed = gameFileCache.scanLibrary(this); 161 if (changed) 162 updateGameFileArray(); 163 hasScannedLibrary.set(true); 164 sendBroadcast(); 165 } 166 } 167 } 168 updateGameFileArray()169 private void updateGameFileArray() 170 { 171 GameFile[] gameFilesTemp = gameFileCache.getAllGames(); 172 Arrays.sort(gameFilesTemp, (lhs, rhs) -> lhs.getTitle().compareToIgnoreCase(rhs.getTitle())); 173 gameFiles.set(gameFilesTemp); 174 } 175 sendBroadcast()176 private void sendBroadcast() 177 { 178 LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(BROADCAST_ACTION)); 179 } 180 } 181