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