Android: Make GameFileCacheService not be a service

The past few Android releases have been adding restrictions
to what services are allowed to do, for the sake of stopping
services from using up too much battery in the background.
The IntentService class, which GameFileCacheService uses,
was even deprecated in Android 11 in light of this.

Typically, the reason why you would want use a service instead of
using a simple thread or some other concurrency mechanism from the
Java standard library is if you want to be able to run code in the
background while the user isn't using your app. This isn't actually
something we care about for GameFileCacheService -- if Android wants
to kill Dolphin there's no reason to keep GameFileCacheService
running -- so let's make it not be a service.

I'm changing this mainly for the sake of future proofing, but there
is one immediate (minor) benefit: Previously, if you tried to launch
Dolphin from Android Studio while your phone was locked, the whole
app would fail to launch because launching GameFileCacheService
wasn't allowed because Dolphin wasn't considered a foreground app.
This commit is contained in:
JosJuice 2021-11-16 21:20:59 +01:00
parent 31bfbca923
commit ffd8cd059c
13 changed files with 77 additions and 107 deletions

View File

@ -132,10 +132,6 @@
android:exported="false"
android:theme="@style/DolphinBase" />
<service
android:name=".services.GameFileCacheService"
android:exported="false"/>
<service
android:name=".services.SyncChannelJobService"
android:exported="false"

View File

@ -14,7 +14,7 @@ import androidx.fragment.app.FragmentActivity;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.dolphinemu.dolphinemu.model.GameFile;
import org.dolphinemu.dolphinemu.services.GameFileCacheService;
import org.dolphinemu.dolphinemu.services.GameFileCacheManager;
import org.dolphinemu.dolphinemu.ui.main.TvMainActivity;
import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner;
import org.dolphinemu.dolphinemu.utils.AppLinkHelper;
@ -69,7 +69,7 @@ public class AppLinkActivity extends FragmentActivity
mAfterDirectoryInitializationRunner = new AfterDirectoryInitializationRunner();
mAfterDirectoryInitializationRunner.run(this, true, () -> tryPlay(playAction));
IntentFilter gameFileCacheIntentFilter = new IntentFilter(GameFileCacheService.DONE_LOADING);
IntentFilter gameFileCacheIntentFilter = new IntentFilter(GameFileCacheManager.DONE_LOADING);
BroadcastReceiver gameFileCacheReceiver = new BroadcastReceiver()
{
@ -87,7 +87,7 @@ public class AppLinkActivity extends FragmentActivity
broadcastManager.registerReceiver(gameFileCacheReceiver, gameFileCacheIntentFilter);
DirectoryInitialization.start(this);
GameFileCacheService.startLoad(this);
GameFileCacheManager.startLoad(this);
}
/**
@ -106,11 +106,11 @@ public class AppLinkActivity extends FragmentActivity
// TODO: This approach of getting the game from the game file cache without rescanning the
// library means that we can fail to launch games if the cache file has been deleted.
GameFile game = GameFileCacheService.getGameFileByGameId(action.getGameId());
GameFile game = GameFileCacheManager.getGameFileByGameId(action.getGameId());
// If game == null and the load isn't done, wait for the next GameFileCacheService broadcast.
// If game == null and the load is done, call play with a null game, making us exit in failure.
if (game != null || !GameFileCacheService.isLoading())
if (game != null || !GameFileCacheManager.isLoading())
{
play(action, game);
}
@ -140,6 +140,6 @@ public class AppLinkActivity extends FragmentActivity
mAfterDirectoryInitializationRunner.cancel();
mAfterDirectoryInitializationRunner = null;
}
EmulationActivity.launch(this, GameFileCacheService.findSecondDiscAndGetPaths(game), false);
EmulationActivity.launch(this, GameFileCacheManager.findSecondDiscAndGetPaths(game), false);
}
}

View File

@ -9,7 +9,6 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.RecyclerView;
@ -17,7 +16,7 @@ import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.activities.EmulationActivity;
import org.dolphinemu.dolphinemu.dialogs.GamePropertiesDialog;
import org.dolphinemu.dolphinemu.model.GameFile;
import org.dolphinemu.dolphinemu.services.GameFileCacheService;
import org.dolphinemu.dolphinemu.services.GameFileCacheManager;
import org.dolphinemu.dolphinemu.utils.PicassoUtils;
import org.dolphinemu.dolphinemu.viewholders.GameViewHolder;
@ -77,7 +76,7 @@ public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> impl
holder.textGameTitle.setText(gameFile.getTitle());
if (GameFileCacheService.findSecondDisc(gameFile) != null)
if (GameFileCacheManager.findSecondDisc(gameFile) != null)
{
holder.textGameCaption
.setText(context.getString(R.string.disc_number, gameFile.getDiscNumber() + 1));
@ -140,7 +139,7 @@ public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> impl
{
GameViewHolder holder = (GameViewHolder) view.getTag();
String[] paths = GameFileCacheService.findSecondDiscAndGetPaths(holder.gameFile);
String[] paths = GameFileCacheManager.findSecondDiscAndGetPaths(holder.gameFile);
EmulationActivity.launch((FragmentActivity) view.getContext(), paths, false);
}

View File

@ -7,7 +7,6 @@ import android.graphics.drawable.Drawable;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentActivity;
import androidx.leanback.widget.ImageCardView;
@ -16,7 +15,7 @@ import androidx.leanback.widget.Presenter;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.dialogs.GamePropertiesDialog;
import org.dolphinemu.dolphinemu.model.GameFile;
import org.dolphinemu.dolphinemu.services.GameFileCacheService;
import org.dolphinemu.dolphinemu.services.GameFileCacheManager;
import org.dolphinemu.dolphinemu.ui.platform.Platform;
import org.dolphinemu.dolphinemu.utils.PicassoUtils;
import org.dolphinemu.dolphinemu.viewholders.TvGameViewHolder;
@ -56,7 +55,7 @@ public final class GameRowPresenter extends Presenter
holder.cardParent.setTitleText(gameFile.getTitle());
if (GameFileCacheService.findSecondDisc(gameFile) != null)
if (GameFileCacheManager.findSecondDisc(gameFile) != null)
{
holder.cardParent
.setContentText(

View File

@ -15,7 +15,7 @@ import androidx.fragment.app.DialogFragment;
import org.dolphinemu.dolphinemu.NativeLibrary;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.model.GameFile;
import org.dolphinemu.dolphinemu.services.GameFileCacheService;
import org.dolphinemu.dolphinemu.services.GameFileCacheManager;
import org.dolphinemu.dolphinemu.utils.PicassoUtils;
public final class GameDetailsDialog extends DialogFragment
@ -36,7 +36,7 @@ public final class GameDetailsDialog extends DialogFragment
@Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
GameFile gameFile = GameFileCacheService.addOrGet(getArguments().getString(ARG_GAME_PATH));
GameFile gameFile = GameFileCacheManager.addOrGet(getArguments().getString(ARG_GAME_PATH));
AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity(),
R.style.DolphinDialogBase);

View File

@ -10,7 +10,7 @@ import org.dolphinemu.dolphinemu.NativeLibrary;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsActivityView;
import org.dolphinemu.dolphinemu.features.settings.utils.SettingsFile;
import org.dolphinemu.dolphinemu.services.GameFileCacheService;
import org.dolphinemu.dolphinemu.services.GameFileCacheManager;
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization;
import org.dolphinemu.dolphinemu.utils.IniFile;
@ -233,7 +233,7 @@ public class Settings implements Closeable
if (mLoadedRecursiveIsoPathsValue != BooleanSetting.MAIN_RECURSIVE_ISO_PATHS.getBoolean(this))
{
// Refresh game library
GameFileCacheService.startRescan(context);
GameFileCacheManager.startRescan(context);
}
}
else

View File

@ -20,7 +20,7 @@ import android.widget.Spinner;
import org.dolphinemu.dolphinemu.NativeLibrary;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.model.GameFile;
import org.dolphinemu.dolphinemu.services.GameFileCacheService;
import org.dolphinemu.dolphinemu.services.GameFileCacheManager;
import org.dolphinemu.dolphinemu.ui.platform.Platform;
import java.io.File;
@ -136,7 +136,7 @@ public class ConvertFragment extends Fragment implements View.OnClickListener
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
gameFile = GameFileCacheService.addOrGet(requireArguments().getString(ARG_GAME_PATH));
gameFile = GameFileCacheManager.addOrGet(requireArguments().getString(ARG_GAME_PATH));
}
@Override

View File

@ -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,7 +111,7 @@ public final class GameFileCacheService extends IntentService
*/
public static boolean isLoading()
{
return unhandledIntents.get() != 0;
return loadInProgress.get();
}
/**
@ -121,14 +119,7 @@ public final class GameFileCacheService extends IntentService
*/
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();
}
/**
@ -138,10 +129,11 @@ public final class GameFileCacheService extends IntentService
*/
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));
}
}
/**
@ -151,11 +143,11 @@ public final class GameFileCacheService extends IntentService
*/
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)
@ -180,33 +172,12 @@ public final class GameFileCacheService extends IntentService
}
}
@Override
protected void onHandleIntent(Intent intent)
{
if (ACTION_LOAD.equals(intent.getAction()))
{
load();
}
if (ACTION_RESCAN.equals(intent.getAction()))
{
rescan();
unhandledRescanIntents.decrementAndGet();
}
int intentsLeft = unhandledIntents.decrementAndGet();
if (intentsLeft == 0)
{
sendBroadcast(DONE_LOADING);
}
}
/**
* 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 void load()
private static void load()
{
if (gameFileCache == null)
{
@ -222,6 +193,10 @@ public final class GameFileCacheService extends IntentService
}
}
}
loadInProgress.set(false);
if (!rescanInProgress.get())
sendBroadcast(DONE_LOADING);
}
/**
@ -229,7 +204,7 @@ public final class GameFileCacheService extends IntentService
* updating the game file cache with the results.
* If load hasn't been called before this, this has no effect.
*/
private void rescan()
private static void rescan()
{
if (gameFileCache != null)
{
@ -258,17 +233,22 @@ public final class GameFileCacheService extends IntentService
gameFileCache.save();
}
}
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));
}
}

View File

@ -111,7 +111,7 @@ public class SyncProgramsJobService extends JobService
private void getGamesByPlatform(Platform platform)
{
updatePrograms = GameFileCacheService.getGameFilesForPlatform(platform);
updatePrograms = GameFileCacheManager.getGameFilesForPlatform(platform);
}
private void syncPrograms(long channelId)

View File

@ -9,7 +9,6 @@ import android.os.Bundle;
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.activities.EmulationActivity;
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 final class MainActivity extends AppCompatActivity
public void onRefresh()
{
setRefreshing(true);
GameFileCacheService.startRescan(this);
GameFileCacheManager.startRescan(this);
}
/**
@ -341,6 +339,6 @@ public final class MainActivity extends AppCompatActivity
mViewPager.setCurrentItem(IntSetting.MAIN_LAST_PLATFORM_TAB.getIntGlobal());
showGames();
GameFileCacheService.startLoad(this);
GameFileCacheManager.startLoad(this);
}
}

View File

@ -18,7 +18,7 @@ import org.dolphinemu.dolphinemu.R;
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,8 +58,8 @@ public final class MainPresenter
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
@ -67,10 +67,10 @@ public final class MainPresenter
{
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 final class MainPresenter
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 final class MainPresenter
mDirToAdd = null;
}
if (sShouldRescanLibrary && !GameFileCacheService.isRescanning())
if (sShouldRescanLibrary && !GameFileCacheManager.isRescanning())
{
new AfterDirectoryInitializationRunner().run(mContext, false, () ->
{
mView.setRefreshing(true);
GameFileCacheService.startRescan(mContext);
GameFileCacheManager.startRescan(mContext);
});
}

View File

@ -7,7 +7,6 @@ import android.content.pm.PackageManager;
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.MenuTag;
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 @@ public final class TvMainActivity extends FragmentActivity
if (DirectoryInitialization.shouldStart(this))
{
DirectoryInitialization.start(this);
GameFileCacheService.startLoad(this);
GameFileCacheManager.startLoad(this);
}
mPresenter.onResume();
@ -124,7 +122,7 @@ public final class TvMainActivity extends FragmentActivity
mSwipeRefresh.setOnRefreshListener(this);
setRefreshing(GameFileCacheService.isLoading());
setRefreshing(GameFileCacheManager.isLoading());
final FragmentManager fragmentManager = getSupportFragmentManager();
mBrowseFragment = new BrowseSupportFragment();
@ -152,7 +150,7 @@ public final class TvMainActivity extends FragmentActivity
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 final class TvMainActivity extends FragmentActivity
}
DirectoryInitialization.start(this);
GameFileCacheService.startLoad(this);
GameFileCacheManager.startLoad(this);
}
}
@ -305,7 +303,7 @@ public final class TvMainActivity extends FragmentActivity
public void onRefresh()
{
setRefreshing(true);
GameFileCacheService.startRescan(this);
GameFileCacheManager.startRescan(this);
}
private void buildRowsAdapter()
@ -315,12 +313,12 @@ public final class TvMainActivity extends FragmentActivity
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)

View File

@ -17,7 +17,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
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 final class PlatformGamesFragment extends Fragment implements PlatformGam
mRecyclerView.addItemDecoration(new GameAdapter.SpacesItemDecoration(8));
setRefreshing(GameFileCacheService.isLoading());
setRefreshing(GameFileCacheManager.isLoading());
showGames();
}
@ -96,7 +96,7 @@ public final class PlatformGamesFragment extends Fragment implements PlatformGam
if (mAdapter != null)
{
Platform platform = (Platform) getArguments().getSerializable(ARG_PLATFORM);
mAdapter.swapDataSet(GameFileCacheService.getGameFilesForPlatform(platform));
mAdapter.swapDataSet(GameFileCacheManager.getGameFilesForPlatform(platform));
}
}