@@ -2,28 +2,29 @@

package org.dolphinemu.dolphinemu.services;

import android.app.IntentService;
import android.content.Context;
import android.content.Intent;

import androidx.localbroadcastmanager.content.LocalBroadcastManager;

import org.dolphinemu.dolphinemu.DolphinApplication;
import org.dolphinemu.dolphinemu.model.GameFile;
import org.dolphinemu.dolphinemu.model.GameFileCache;
import org.dolphinemu.dolphinemu.ui.platform.Platform;
import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

/**
* A service that loads game list data on a separate thread.
* Loads game list data on a separate thread.
*/
public final class GameFileCacheService extends IntentService
public final class GameFileCacheManager
{
/**
* This is broadcast when the contents of the cache change.
@@ -37,19 +38,16 @@ public final class GameFileCacheService extends IntentService
public static final String DONE_LOADING =
"org.dolphinemu.dolphinemu.GAME_FILE_CACHE_DONE_LOADING";

private static final String ACTION_LOAD = "org.dolphinemu.dolphinemu.LOAD_GAME_FILE_CACHE";
private static final String ACTION_RESCAN = "org.dolphinemu.dolphinemu.RESCAN_GAME_FILE_CACHE";

private static GameFileCache gameFileCache = null;
private static final AtomicReference<GameFile[]> gameFiles =
new AtomicReference<>(new GameFile[]{});
private static final AtomicInteger unhandledIntents = new AtomicInteger(0);
private static final AtomicInteger unhandledRescanIntents = new AtomicInteger(0);

public GameFileCacheService()
private static final ExecutorService executor = Executors.newFixedThreadPool(1);
private static final AtomicBoolean loadInProgress = new AtomicBoolean(false);
private static final AtomicBoolean rescanInProgress = new AtomicBoolean(false);

private GameFileCacheManager()
{
// Superclass constructor is called to name the thread on which this service executes.
super("GameFileCacheService");
}

public static List<GameFile> getGameFilesForPlatform(Platform platform)
@@ -113,34 +111,29 @@ public static String[] findSecondDiscAndGetPaths(GameFile gameFile)
*/
public static boolean isLoading()
{
return unhandledIntents.get() != 0;
return loadInProgress.get();
}

/**
* Returns true if in the process of rescanning.
*/
public static boolean isRescanning()
{
return unhandledRescanIntents.get() != 0;
}

private static void startService(Context context, String action)
{
Intent intent = new Intent(context, GameFileCacheService.class);
intent.setAction(action);
context.startService(intent);
return rescanInProgress.get();
}

/**
* Asynchronously loads the game file cache from disk without checking
* which games are present on the file system.
* 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(Context context)
{
unhandledIntents.getAndIncrement();

new AfterDirectoryInitializationRunner().run(context, false,
() -> startService(context, ACTION_LOAD));
if (loadInProgress.compareAndSet(false, true))
{
new AfterDirectoryInitializationRunner().run(context, false,
() -> executor.execute(GameFileCacheManager::load));
}
}

/**
@@ -150,11 +143,11 @@ public static void startLoad(Context context)
*/
public static void startRescan(Context context)
{
unhandledIntents.getAndIncrement();
unhandledRescanIntents.getAndIncrement();

new AfterDirectoryInitializationRunner().run(context, false,
() -> startService(context, ACTION_RESCAN));
if (rescanInProgress.compareAndSet(false, true))
{
new AfterDirectoryInitializationRunner().run(context, false,
() -> executor.execute(GameFileCacheManager::rescan));
}
}

public static GameFile addOrGet(String gamePath)
@@ -179,11 +172,14 @@ public static GameFile addOrGet(String gamePath)
}
}

@Override
protected void onHandleIntent(Intent intent)
/**
* 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()
{
// Load the game list cache if it isn't already loaded, otherwise do nothing
if (ACTION_LOAD.equals(intent.getAction()) && gameFileCache == null)
if (gameFileCache == null)
{
GameFileCache temp = new GameFileCache();
synchronized (temp)
@@ -198,56 +194,61 @@ protected void onHandleIntent(Intent intent)
}
}

// Rescan the file system and update the game list cache with the results
if (ACTION_RESCAN.equals(intent.getAction()))
{
if (gameFileCache != null)
{
String[] gamePaths = GameFileCache.getAllGamePaths();
loadInProgress.set(false);
if (!rescanInProgress.get())
sendBroadcast(DONE_LOADING);
}

boolean changed;
synchronized (gameFileCache)
{
changed = gameFileCache.update(gamePaths);
}
if (changed)
{
updateGameFileArray();
sendBroadcast(CACHE_UPDATED);
}
/**
* 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, this has no effect.
*/
private static void rescan()
{
if (gameFileCache != null)
{
String[] gamePaths = GameFileCache.getAllGamePaths();

boolean additionalMetadataChanged = gameFileCache.updateAdditionalMetadata();
if (additionalMetadataChanged)
{
updateGameFileArray();
sendBroadcast(CACHE_UPDATED);
}
boolean changed;
synchronized (gameFileCache)
{
changed = gameFileCache.update(gamePaths);
}
if (changed)
{
updateGameFileArray();
sendBroadcast(CACHE_UPDATED);
}

if (changed || additionalMetadataChanged)
{
gameFileCache.save();
}
boolean additionalMetadataChanged = gameFileCache.updateAdditionalMetadata();
if (additionalMetadataChanged)
{
updateGameFileArray();
sendBroadcast(CACHE_UPDATED);
}

unhandledRescanIntents.decrementAndGet();
if (changed || additionalMetadataChanged)
{
gameFileCache.save();
}
}

int intentsLeft = unhandledIntents.decrementAndGet();
if (intentsLeft == 0)
{
rescanInProgress.set(false);
if (!loadInProgress.get())
sendBroadcast(DONE_LOADING);
}
}

private void updateGameFileArray()
private static void updateGameFileArray()
{
GameFile[] gameFilesTemp = gameFileCache.getAllGames();
Arrays.sort(gameFilesTemp, (lhs, rhs) -> lhs.getTitle().compareToIgnoreCase(rhs.getTitle()));
gameFiles.set(gameFilesTemp);
}

private void sendBroadcast(String action)
private static void sendBroadcast(String action)
{
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(action));
LocalBroadcastManager.getInstance(DolphinApplication.getAppContext())
.sendBroadcast(new Intent(action));
}
}
@@ -111,7 +111,7 @@ protected Boolean doInBackground(Long... channelIds)

private void getGamesByPlatform(Platform platform)
{
updatePrograms = GameFileCacheService.getGameFilesForPlatform(platform);
updatePrograms = GameFileCacheManager.getGameFilesForPlatform(platform);
}

private void syncPrograms(long channelId)
@@ -9,7 +9,6 @@
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -26,10 +25,9 @@
import org.dolphinemu.dolphinemu.adapters.PlatformPagerAdapter;
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting;
import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig;
import org.dolphinemu.dolphinemu.features.settings.model.Settings;
import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag;
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsActivity;
import org.dolphinemu.dolphinemu.services.GameFileCacheService;
import org.dolphinemu.dolphinemu.services.GameFileCacheManager;
import org.dolphinemu.dolphinemu.ui.platform.Platform;
import org.dolphinemu.dolphinemu.ui.platform.PlatformGamesView;
import org.dolphinemu.dolphinemu.utils.Action1;
@@ -279,7 +277,7 @@ public boolean onOptionsItemSelected(MenuItem item)
public void onRefresh()
{
setRefreshing(true);
GameFileCacheService.startRescan(this);
GameFileCacheManager.startRescan(this);
}

/**
@@ -341,6 +339,6 @@ public void onTabSelected(@NonNull TabLayout.Tab tab)
mViewPager.setCurrentItem(IntSetting.MAIN_LAST_PLATFORM_TAB.getIntGlobal());

showGames();
GameFileCacheService.startLoad(this);
GameFileCacheManager.startLoad(this);
}
}
@@ -18,7 +18,7 @@
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting;
import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag;
import org.dolphinemu.dolphinemu.model.GameFileCache;
import org.dolphinemu.dolphinemu.services.GameFileCacheService;
import org.dolphinemu.dolphinemu.services.GameFileCacheManager;
import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner;
import org.dolphinemu.dolphinemu.utils.BooleanSupplier;
import org.dolphinemu.dolphinemu.utils.CompletableFuture;
@@ -58,19 +58,19 @@ public void onCreate()
mView.setVersionString(versionName);

IntentFilter filter = new IntentFilter();
filter.addAction(GameFileCacheService.CACHE_UPDATED);
filter.addAction(GameFileCacheService.DONE_LOADING);
filter.addAction(GameFileCacheManager.CACHE_UPDATED);
filter.addAction(GameFileCacheManager.DONE_LOADING);
mBroadcastReceiver = new BroadcastReceiver()
{
@Override
public void onReceive(Context context, Intent intent)
{
switch (intent.getAction())
{
case GameFileCacheService.CACHE_UPDATED:
case GameFileCacheManager.CACHE_UPDATED:
mView.showGames();
break;
case GameFileCacheService.DONE_LOADING:
case GameFileCacheManager.DONE_LOADING:
mView.setRefreshing(false);
break;
}
@@ -102,7 +102,7 @@ public boolean handleOptionSelection(int itemId, Context context)

case R.id.menu_refresh:
mView.setRefreshing(true);
GameFileCacheService.startRescan(context);
GameFileCacheManager.startRescan(context);
return true;

case R.id.button_add_directory:
@@ -140,12 +140,12 @@ public void onResume()
mDirToAdd = null;
}

if (sShouldRescanLibrary && !GameFileCacheService.isRescanning())
if (sShouldRescanLibrary && !GameFileCacheManager.isRescanning())
{
new AfterDirectoryInitializationRunner().run(mContext, false, () ->
{
mView.setRefreshing(true);
GameFileCacheService.startRescan(mContext);
GameFileCacheManager.startRescan(mContext);
});
}

@@ -7,7 +7,6 @@
import android.net.Uri;
import android.os.Bundle;
import android.util.TypedValue;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
@@ -28,9 +27,8 @@
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsActivity;
import org.dolphinemu.dolphinemu.model.GameFile;
import org.dolphinemu.dolphinemu.model.TvSettingsItem;
import org.dolphinemu.dolphinemu.services.GameFileCacheService;
import org.dolphinemu.dolphinemu.services.GameFileCacheManager;
import org.dolphinemu.dolphinemu.ui.platform.Platform;
import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner;
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization;
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper;
import org.dolphinemu.dolphinemu.utils.PermissionsHandler;
@@ -77,7 +75,7 @@ protected void onResume()
if (DirectoryInitialization.shouldStart(this))
{
DirectoryInitialization.start(this);
GameFileCacheService.startLoad(this);
GameFileCacheManager.startLoad(this);
}

mPresenter.onResume();
@@ -124,7 +122,7 @@ void setupUI()

mSwipeRefresh.setOnRefreshListener(this);

setRefreshing(GameFileCacheService.isLoading());
setRefreshing(GameFileCacheManager.isLoading());

final FragmentManager fragmentManager = getSupportFragmentManager();
mBrowseFragment = new BrowseSupportFragment();
@@ -152,7 +150,7 @@ void setupUI()
TvGameViewHolder holder = (TvGameViewHolder) itemViewHolder;

// Start the emulation activity and send the path of the clicked ISO to it.
String[] paths = GameFileCacheService.findSecondDiscAndGetPaths(holder.gameFile);
String[] paths = GameFileCacheManager.findSecondDiscAndGetPaths(holder.gameFile);
EmulationActivity.launch(TvMainActivity.this, paths, false);
}
});
@@ -294,7 +292,7 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis
}

DirectoryInitialization.start(this);
GameFileCacheService.startLoad(this);
GameFileCacheManager.startLoad(this);
}
}

@@ -305,7 +303,7 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis
public void onRefresh()
{
setRefreshing(true);
GameFileCacheService.startRescan(this);
GameFileCacheManager.startRescan(this);
}

private void buildRowsAdapter()
@@ -315,12 +313,12 @@ private void buildRowsAdapter()

if (!DirectoryInitialization.isWaitingForWriteAccess(this))
{
GameFileCacheService.startLoad(this);
GameFileCacheManager.startLoad(this);
}

for (Platform platform : Platform.values())
{
ListRow row = buildGamesRow(platform, GameFileCacheService.getGameFilesForPlatform(platform));
ListRow row = buildGamesRow(platform, GameFileCacheManager.getGameFilesForPlatform(platform));

// Add row to the adapter only if it is not empty.
if (row != null)
@@ -17,7 +17,7 @@

import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.adapters.GameAdapter;
import org.dolphinemu.dolphinemu.services.GameFileCacheService;
import org.dolphinemu.dolphinemu.services.GameFileCacheManager;

public final class PlatformGamesFragment extends Fragment implements PlatformGamesView
{
@@ -73,7 +73,7 @@ public void onViewCreated(@NonNull View view, Bundle savedInstanceState)

mRecyclerView.addItemDecoration(new GameAdapter.SpacesItemDecoration(8));

setRefreshing(GameFileCacheService.isLoading());
setRefreshing(GameFileCacheManager.isLoading());

showGames();
}
@@ -96,7 +96,7 @@ public void showGames()
if (mAdapter != null)
{
Platform platform = (Platform) getArguments().getSerializable(ARG_PLATFORM);
mAdapter.swapDataSet(GameFileCacheService.getGameFilesForPlatform(platform));
mAdapter.swapDataSet(GameFileCacheManager.getGameFilesForPlatform(platform));
}
}