diff --git a/Source/Android/app/src/main/AndroidManifest.xml b/Source/Android/app/src/main/AndroidManifest.xml index 5d817d220a..aa0b121c5c 100644 --- a/Source/Android/app/src/main/AndroidManifest.xml +++ b/Source/Android/app/src/main/AndroidManifest.xml @@ -71,13 +71,7 @@ - - - + implements View.OnClickListener, View.OnLongClickListener { - private Cursor mCursor; - private GameDataSetObserver mObserver; - - private boolean mDatasetValid; + private List mGameFiles; /** * Initializes the adapter's observer, which watches for changes to the dataset. The adapter will - * display no data until a Cursor is supplied by a CursorLoader. + * display no data until swapDataSet is called. */ public GameAdapter() { - mDatasetValid = false; - mObserver = new GameDataSetObserver(); + mGameFiles = new ArrayList<>(); } /** @@ -80,34 +70,13 @@ public final class GameAdapter extends RecyclerView.Adapter impl @Override public void onBindViewHolder(GameViewHolder holder, int position) { - if (mDatasetValid) - { - if (mCursor.moveToPosition(position)) - { - String screenPath = mCursor.getString(GameDatabase.GAME_COLUMN_SCREENSHOT_PATH); - PicassoUtils.loadGameBanner(holder.imageScreenshot, screenPath, mCursor.getString(GameDatabase.GAME_COLUMN_PATH)); + GameFile gameFile = mGameFiles.get(position); + PicassoUtils.loadGameBanner(holder.imageScreenshot, gameFile); - holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE)); - holder.textCompany.setText(mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY)); + holder.textGameTitle.setText(gameFile.getTitle()); + holder.textCompany.setText(gameFile.getCompany()); - // TODO These shouldn't be necessary once the move to a DB-based model is complete. - holder.gameId = mCursor.getString(GameDatabase.GAME_COLUMN_GAME_ID); - holder.path = mCursor.getString(GameDatabase.GAME_COLUMN_PATH); - holder.title = mCursor.getString(GameDatabase.GAME_COLUMN_TITLE); - holder.description = mCursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION); - holder.country = mCursor.getInt(GameDatabase.GAME_COLUMN_COUNTRY); - holder.company = mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY); - holder.screenshotPath = mCursor.getString(GameDatabase.GAME_COLUMN_SCREENSHOT_PATH); - } - else - { - Log.error("[GameAdapter] Can't bind view; Cursor is not valid."); - } - } - else - { - Log.error("[GameAdapter] Can't bind view; dataset is not valid."); - } + holder.gameFile = gameFile; } /** @@ -118,84 +87,27 @@ public final class GameAdapter extends RecyclerView.Adapter impl @Override public int getItemCount() { - if (mDatasetValid && mCursor != null) - { - return mCursor.getCount(); - } - Log.error("[GameAdapter] Dataset is not valid."); - return 0; - } - - /** - * Return the contents of the _id column for a given row. - * - * @param position The row for which Android wants an ID. - * @return A valid ID from the database, or 0 if not available. - */ - @Override - public long getItemId(int position) - { - if (mDatasetValid && mCursor != null) - { - if (mCursor.moveToPosition(position)) - { - return mCursor.getLong(GameDatabase.COLUMN_DB_ID); - } - } - - Log.error("[GameAdapter] Dataset is not valid."); - return 0; + return mGameFiles.size(); } /** * Tell Android whether or not each item in the dataset has a stable identifier. - * Which it does, because it's a database, so always tell Android 'true'. * * @param hasStableIds ignored. */ @Override public void setHasStableIds(boolean hasStableIds) { - super.setHasStableIds(true); + super.setHasStableIds(false); } /** - * When a load is finished, call this to replace the existing data with the newly-loaded - * data. - * - * @param cursor The newly-loaded Cursor. + * When a load is finished, call this to replace the existing data + * with the newly-loaded data. */ - public void swapCursor(Cursor cursor) + public void swapDataSet(List gameFiles) { - // Sanity check. - if (cursor == mCursor) - { - return; - } - - // Before getting rid of the old cursor, disassociate it from the Observer. - final Cursor oldCursor = mCursor; - if (oldCursor != null && mObserver != null) - { - oldCursor.unregisterDataSetObserver(mObserver); - } - - mCursor = cursor; - if (mCursor != null) - { - // Attempt to associate the new Cursor with the Observer. - if (mObserver != null) - { - mCursor.registerDataSetObserver(mObserver); - } - - mDatasetValid = true; - } - else - { - mDatasetValid = false; - } - + mGameFiles = gameFiles; notifyDataSetChanged(); } @@ -210,9 +122,7 @@ public final class GameAdapter extends RecyclerView.Adapter impl GameViewHolder holder = (GameViewHolder) view.getTag(); EmulationActivity.launch((FragmentActivity) view.getContext(), - holder.path, - holder.title, - holder.screenshotPath, + holder.gameFile, holder.getAdapterPosition(), holder.imageScreenshot); } @@ -227,13 +137,10 @@ public final class GameAdapter extends RecyclerView.Adapter impl public boolean onLongClick(View view) { GameViewHolder holder = (GameViewHolder) view.getTag(); - - // Get the ID of the game we want to look at. - String gameId = (String) holder.gameId; + String gameId = holder.gameFile.getGameId(); FragmentActivity activity = (FragmentActivity) view.getContext(); - AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setTitle("Game Settings") .setItems(R.array.gameSettingsMenus, new DialogInterface.OnClickListener() { @@ -290,25 +197,4 @@ public final class GameAdapter extends RecyclerView.Adapter impl outRect.top = space; } } - - private final class GameDataSetObserver extends DataSetObserver - { - @Override - public void onChanged() - { - super.onChanged(); - - mDatasetValid = true; - notifyDataSetChanged(); - } - - @Override - public void onInvalidated() - { - super.onInvalidated(); - - mDatasetValid = false; - notifyDataSetChanged(); - } - } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.java index 81234904da..79c1fabf9b 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.java @@ -14,8 +14,9 @@ import android.widget.ImageView; import android.widget.Toast; import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.model.Game; +import org.dolphinemu.dolphinemu.model.GameFile; import org.dolphinemu.dolphinemu.services.DirectoryInitializationService; +import org.dolphinemu.dolphinemu.ui.platform.Platform; import org.dolphinemu.dolphinemu.ui.settings.SettingsActivity; import org.dolphinemu.dolphinemu.utils.PicassoUtils; import org.dolphinemu.dolphinemu.utils.SettingsFile; @@ -50,28 +51,19 @@ public final class GameRowPresenter extends Presenter public void onBindViewHolder(ViewHolder viewHolder, Object item) { TvGameViewHolder holder = (TvGameViewHolder) viewHolder; - Game game = (Game) item; - - String screenPath = game.getScreenshotPath(); + GameFile gameFile = (GameFile) item; holder.imageScreenshot.setImageDrawable(null); - PicassoUtils.loadGameBanner(holder.imageScreenshot, screenPath, game.getPath()); + PicassoUtils.loadGameBanner(holder.imageScreenshot, gameFile); - holder.cardParent.setTitleText(game.getTitle()); - holder.cardParent.setContentText(game.getCompany()); + holder.cardParent.setTitleText(gameFile.getTitle()); + holder.cardParent.setContentText(gameFile.getCompany()); - // TODO These shouldn't be necessary once the move to a DB-based model is complete. - holder.gameId = game.getGameId(); - holder.path = game.getPath(); - holder.title = game.getTitle(); - holder.description = game.getDescription(); - holder.country = game.getCountry(); - holder.company = game.getCompany(); - holder.screenshotPath = game.getScreenshotPath(); + holder.gameFile = gameFile; // Set the platform-dependent background color of the card int backgroundId; - switch (game.getPlatform()) + switch (Platform.fromNativeInt(gameFile.getPlatform())) { case GAMECUBE: backgroundId = R.drawable.tv_card_background_gamecube; @@ -93,7 +85,7 @@ public final class GameRowPresenter extends Presenter public boolean onLongClick(View view) { FragmentActivity activity = (FragmentActivity) view.getContext(); - + String gameId = gameFile.getGameId(); AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setTitle("Game Settings") @@ -101,23 +93,23 @@ public final class GameRowPresenter extends Presenter public void onClick(DialogInterface dialog, int which) { switch (which) { case 0: - SettingsActivity.launch(activity, SettingsFile.FILE_NAME_DOLPHIN, game.getGameId()); + SettingsActivity.launch(activity, SettingsFile.FILE_NAME_DOLPHIN, gameId); break; case 1: - SettingsActivity.launch(activity, SettingsFile.FILE_NAME_GFX, game.getGameId()); + SettingsActivity.launch(activity, SettingsFile.FILE_NAME_GFX, gameId); break; case 2: - String path = DirectoryInitializationService.getUserDirectory() + "/GameSettings/" + game.getGameId() + ".ini"; + String path = DirectoryInitializationService.getUserDirectory() + "/GameSettings/" + gameId + ".ini"; File gameSettingsFile = new File(path); if (gameSettingsFile.exists()) { if (gameSettingsFile.delete()) { - Toast.makeText(view.getContext(), "Cleared settings for " + game.getGameId(), Toast.LENGTH_SHORT).show(); + Toast.makeText(view.getContext(), "Cleared settings for " + gameId, Toast.LENGTH_SHORT).show(); } else { - Toast.makeText(view.getContext(), "Unable to clear settings for " + game.getGameId(), Toast.LENGTH_SHORT).show(); + Toast.makeText(view.getContext(), "Unable to clear settings for " + gameId, Toast.LENGTH_SHORT).show(); } } else diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.java index b3dc13de22..718c03152d 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.java @@ -12,32 +12,24 @@ import android.widget.TextView; import com.squareup.picasso.Picasso; +import org.dolphinemu.dolphinemu.DolphinApplication; import org.dolphinemu.dolphinemu.R; import org.dolphinemu.dolphinemu.activities.EmulationActivity; +import org.dolphinemu.dolphinemu.model.GameFile; +import org.dolphinemu.dolphinemu.services.GameFileCacheService; import de.hdodenhof.circleimageview.CircleImageView; public final class GameDetailsDialog extends DialogFragment { - private static final String ARG_GAME_TITLE = "game_title"; - private static final String ARG_GAME_DESCRIPTION = "game_description"; - private static final String ARG_GAME_COUNTRY = "game_country"; - private static final String ARG_GAME_DATE = "game_date"; private static final String ARG_GAME_PATH = "game_path"; - private static final String ARG_GAME_SCREENSHOT_PATH = "game_screenshot_path"; - // TODO Add all of this to the Loader in GameActivity.java - public static GameDetailsDialog newInstance(String title, String description, int country, String company, String path, String screenshotPath) + public static GameDetailsDialog newInstance(String gamePath) { GameDetailsDialog fragment = new GameDetailsDialog(); Bundle arguments = new Bundle(); - arguments.putString(ARG_GAME_TITLE, title); - arguments.putString(ARG_GAME_DESCRIPTION, description); - arguments.putInt(ARG_GAME_COUNTRY, country); - arguments.putString(ARG_GAME_DATE, company); - arguments.putString(ARG_GAME_PATH, path); - arguments.putString(ARG_GAME_SCREENSHOT_PATH, screenshotPath); + arguments.putString(ARG_GAME_PATH, gamePath); fragment.setArguments(arguments); return fragment; @@ -46,42 +38,38 @@ public final class GameDetailsDialog extends DialogFragment @Override public Dialog onCreateDialog(Bundle savedInstanceState) { + GameFile gameFile = GameFileCacheService.addOrGet(getArguments().getString(ARG_GAME_PATH)); + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); ViewGroup contents = (ViewGroup) getActivity().getLayoutInflater().inflate(R.layout.dialog_game_details, null); - final ImageView imageGameScreen = (ImageView) contents.findViewById(R.id.image_game_screen); - CircleImageView circleBanner = (CircleImageView) contents.findViewById(R.id.circle_banner); + final ImageView imageGameScreen = contents.findViewById(R.id.image_game_screen); + CircleImageView circleBanner = contents.findViewById(R.id.circle_banner); - TextView textTitle = (TextView) contents.findViewById(R.id.text_game_title); - TextView textDescription = (TextView) contents.findViewById(R.id.text_company); + TextView textTitle = contents.findViewById(R.id.text_game_title); + TextView textDescription = contents.findViewById(R.id.text_description); - TextView textCountry = (TextView) contents.findViewById(R.id.text_country); - TextView textDate = (TextView) contents.findViewById(R.id.text_date); + TextView textCountry = contents.findViewById(R.id.text_country); + TextView textCompany = contents.findViewById(R.id.text_company); - FloatingActionButton buttonLaunch = (FloatingActionButton) contents.findViewById(R.id.button_launch); + FloatingActionButton buttonLaunch = contents.findViewById(R.id.button_launch); - int countryIndex = getArguments().getInt(ARG_GAME_COUNTRY); - String country = getResources().getStringArray(R.array.countryNames)[countryIndex]; + String country = getResources().getStringArray(R.array.countryNames)[gameFile.getCountry()]; - textTitle.setText(getArguments().getString(ARG_GAME_TITLE)); - textDescription.setText(getArguments().getString(ARG_GAME_DESCRIPTION)); + textTitle.setText(gameFile.getTitle()); + textDescription.setText(gameFile.getDescription()); textCountry.setText(country); - textDate.setText(getArguments().getString(ARG_GAME_DATE)); + textCompany.setText(gameFile.getCompany()); buttonLaunch.setOnClickListener(view -> { // Start the emulation activity and send the path of the clicked ROM to it. - EmulationActivity.launch(getActivity(), - getArguments().getString(ARG_GAME_PATH), - getArguments().getString(ARG_GAME_TITLE), - getArguments().getString(ARG_GAME_SCREENSHOT_PATH), - -1, - imageGameScreen); + EmulationActivity.launch(getActivity(), gameFile, -1, imageGameScreen); }); // Fill in the view contents. Picasso.with(imageGameScreen.getContext()) - .load(getArguments().getString(ARG_GAME_SCREENSHOT_PATH)) + .load(getArguments().getString(gameFile.getScreenshotPath())) .fit() .centerCrop() .noFade() diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/FileListItem.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/FileListItem.java deleted file mode 100644 index 4460a1e155..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/FileListItem.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.dolphinemu.dolphinemu.model; - -import org.dolphinemu.dolphinemu.NativeLibrary; - -import java.io.File; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -public class FileListItem implements Comparable -{ - public static final int TYPE_FOLDER = 0; - public static final int TYPE_GC = 1; - public static final int TYPE_WII = 2; - public static final int TYPE_WII_WARE = 3; - public static final int TYPE_OTHER = 4; - - private int mType; - private String mFilename; - private String mPath; - - public FileListItem(File file) - { - mPath = file.getAbsolutePath(); - mFilename = file.getName(); - - if (file.isDirectory()) - { - mType = TYPE_FOLDER; - } - else - { - int extensionStart = mPath.lastIndexOf('.'); - if (extensionStart < 1) - { - // Ignore hidden files & files without extensions. - mType = TYPE_OTHER; - } - else - { - String fileExtension = mPath.substring(extensionStart); - - // The extensions we care about. - Set allowedExtensions = new HashSet(Arrays.asList( - ".ciso", ".dff", ".dol", ".elf", ".gcm", ".gcz", ".iso", ".tgc", ".wad", ".wbfs")); - - // Check that the file has an extension we care about before trying to read out of it. - if (allowedExtensions.contains(fileExtension.toLowerCase())) - { - // Add 1 because 0 = TYPE_FOLDER - mType = NativeLibrary.GetPlatform(mPath) + 1; - } - else - { - mType = TYPE_OTHER; - } - } - } - } - - public int getType() - { - return mType; - } - - public String getFilename() - { - return mFilename; - } - - public String getPath() - { - return mPath; - } - - @Override - public int compareTo(FileListItem theOther) - { - if (theOther.getType() == getType()) - { - return getFilename().toLowerCase().compareTo(theOther.getFilename().toLowerCase()); - } - else - { - if (getType() > theOther.getType()) - { - return 1; - } - else - { - return -1; - } - } - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/Game.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/Game.java deleted file mode 100644 index c38bcef2e1..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/Game.java +++ /dev/null @@ -1,120 +0,0 @@ -package org.dolphinemu.dolphinemu.model; - -import android.content.ContentValues; -import android.database.Cursor; -import android.os.Environment; - -import org.dolphinemu.dolphinemu.ui.platform.Platform; - -public final class Game -{ - // Copied from IVolume::ECountry. Update these if that is ever modified. - public static final int COUNTRY_EUROPE = 0; - public static final int COUNTRY_JAPAN = 1; - public static final int COUNTRY_USA = 2; - public static final int COUNTRY_AUSTRALIA = 3; - public static final int COUNTRY_FRANCE = 4; - public static final int COUNTRY_GERMANY = 5; - public static final int COUNTRY_ITALY = 6; - public static final int COUNTRY_KOREA = 7; - public static final int COUNTRY_NETHERLANDS = 8; - public static final int COUNTRY_RUSSIA = 9; - public static final int COUNTRY_SPAIN = 10; - public static final int COUNTRY_TAIWAN = 11; - public static final int COUNTRY_WORLD = 12; - public static final int COUNTRY_UNKNOWN = 13; - - private static final String PATH_SCREENSHOT_FOLDER = "file://" + Environment.getExternalStorageDirectory().getPath() + "/dolphin-emu/ScreenShots/"; - - private String mTitle; - private String mDescription; - private String mPath; - private String mGameId; - private String mScreenshotPath; - private String mCompany; - - private Platform mPlatform; - private int mCountry; - - public Game(Platform platform, String title, String description, int country, String path, String gameId, String company, String screenshotPath) - { - mPlatform = platform; - mTitle = title; - mDescription = description; - mCountry = country; - mPath = path; - mGameId = gameId; - mCompany = company; - mScreenshotPath = screenshotPath; - } - - public Platform getPlatform() - { - return mPlatform; - } - - public String getTitle() - { - return mTitle; - } - - public String getDescription() - { - return mDescription; - } - - public String getCompany() - { - return mCompany; - } - - public int getCountry() - { - return mCountry; - } - - public String getPath() - { - return mPath; - } - - public String getGameId() - { - return mGameId; - } - - public String getScreenshotPath() - { - return mScreenshotPath; - } - - public static ContentValues asContentValues(Platform platform, String title, String description, int country, String path, String gameId, String company) - { - ContentValues values = new ContentValues(); - - String screenPath = PATH_SCREENSHOT_FOLDER + gameId + "/" + gameId + "-1.png"; - - values.put(GameDatabase.KEY_GAME_PLATFORM, platform.toInt()); - values.put(GameDatabase.KEY_GAME_TITLE, title); - values.put(GameDatabase.KEY_GAME_DESCRIPTION, description); - values.put(GameDatabase.KEY_GAME_COUNTRY, company); - values.put(GameDatabase.KEY_GAME_PATH, path); - values.put(GameDatabase.KEY_GAME_ID, gameId); - values.put(GameDatabase.KEY_GAME_COMPANY, company); - values.put(GameDatabase.KEY_GAME_SCREENSHOT_PATH, screenPath); - - return values; - } - - public static Game fromCursor(Cursor cursor) - { - return new Game(Platform.fromInt(cursor.getInt(GameDatabase.GAME_COLUMN_PLATFORM)), - cursor.getString(GameDatabase.GAME_COLUMN_TITLE), - cursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION), - cursor.getInt(GameDatabase.GAME_COLUMN_COUNTRY), - cursor.getString(GameDatabase.GAME_COLUMN_PATH), - cursor.getString(GameDatabase.GAME_COLUMN_GAME_ID), - cursor.getString(GameDatabase.GAME_COLUMN_COMPANY), - cursor.getString(GameDatabase.GAME_COLUMN_SCREENSHOT_PATH)); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameDatabase.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameDatabase.java deleted file mode 100644 index 9c152d90ad..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameDatabase.java +++ /dev/null @@ -1,299 +0,0 @@ -package org.dolphinemu.dolphinemu.model; - -import android.content.ContentValues; -import android.content.Context; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.preference.PreferenceManager; - -import org.dolphinemu.dolphinemu.NativeLibrary; -import org.dolphinemu.dolphinemu.ui.platform.Platform; -import org.dolphinemu.dolphinemu.utils.Log; - -import java.io.File; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -import rx.Observable; -import rx.Subscriber; - -/** - * A helper class that provides several utilities simplifying interaction with - * the SQLite database. - */ -public final class GameDatabase extends SQLiteOpenHelper -{ - private static final int DB_VERSION = 1; - - public static final int COLUMN_DB_ID = 0; - - public static final int GAME_COLUMN_PATH = 1; - public static final int GAME_COLUMN_PLATFORM = 2; - public static final int GAME_COLUMN_TITLE = 3; - public static final int GAME_COLUMN_DESCRIPTION = 4; - public static final int GAME_COLUMN_COUNTRY = 5; - public static final int GAME_COLUMN_GAME_ID = 6; - public static final int GAME_COLUMN_COMPANY = 7; - public static final int GAME_COLUMN_SCREENSHOT_PATH = 8; - - public static final String KEY_DB_ID = "_id"; - - public static final String KEY_GAME_PATH = "path"; - public static final String KEY_GAME_PLATFORM = "platform"; - public static final String KEY_GAME_TITLE = "title"; - public static final String KEY_GAME_DESCRIPTION = "description"; - public static final String KEY_GAME_COUNTRY = "country"; - public static final String KEY_GAME_ID = "game_id"; - public static final String KEY_GAME_COMPANY = "company"; - public static final String KEY_GAME_SCREENSHOT_PATH = "screenshot_path"; - public static final String TABLE_NAME_GAMES = "games"; - - private static final String TYPE_PRIMARY = " INTEGER PRIMARY KEY"; - private static final String TYPE_INTEGER = " INTEGER"; - private static final String TYPE_STRING = " TEXT"; - - private static final String SEPARATOR = ", "; - - private static final String SQL_CREATE_GAMES = "CREATE TABLE " + TABLE_NAME_GAMES + "(" - + KEY_DB_ID + TYPE_PRIMARY + SEPARATOR - + KEY_GAME_PATH + TYPE_STRING + SEPARATOR - + KEY_GAME_PLATFORM + TYPE_STRING + SEPARATOR - + KEY_GAME_TITLE + TYPE_STRING + SEPARATOR - + KEY_GAME_DESCRIPTION + TYPE_STRING + SEPARATOR - + KEY_GAME_COUNTRY + TYPE_INTEGER + SEPARATOR - + KEY_GAME_ID + TYPE_STRING + SEPARATOR - + KEY_GAME_COMPANY + TYPE_STRING + SEPARATOR - + KEY_GAME_SCREENSHOT_PATH + TYPE_STRING + ")"; - - private static final String SQL_DELETE_GAMES = "DROP TABLE IF EXISTS " + TABLE_NAME_GAMES; - - private static final String GAME_FOLDER_PATHS_PREFERENCE = "gameFolderPaths"; - - private static final Set EMPTY_SET = new HashSet<>(); - - private Context mContext; - - public GameDatabase(Context context) - { - // Superclass constructor builds a database or uses an existing one. - super(context, "games.db", null, DB_VERSION); - mContext = context; - } - - @Override - public void onCreate(SQLiteDatabase database) - { - Log.debug("[GameDatabase] GameDatabase - Creating database..."); - - execSqlAndLog(database, SQL_CREATE_GAMES); - } - - @Override - public void onDowngrade(SQLiteDatabase database, int oldVersion, int newVersion) - { - Log.verbose("[GameDatabase] Downgrades not supporting, clearing databases.."); - execSqlAndLog(database, SQL_DELETE_GAMES); - execSqlAndLog(database, SQL_CREATE_GAMES); - } - - @Override - public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion) - { - Log.info("[GameDatabase] Upgrading database from schema version " + oldVersion + " to " + newVersion); - - // Delete all the games - execSqlAndLog(database, SQL_DELETE_GAMES); - execSqlAndLog(database, SQL_CREATE_GAMES); - - Log.verbose("[GameDatabase] Re-scanning library with new schema."); - scanLibrary(database); - } - - public void addGameFolder(String path) - { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext); - Set folderPaths = preferences.getStringSet(GAME_FOLDER_PATHS_PREFERENCE, EMPTY_SET); - Set newFolderPaths = new HashSet<>(folderPaths); - newFolderPaths.add(path); - SharedPreferences.Editor editor = preferences.edit(); - editor.putStringSet(GAME_FOLDER_PATHS_PREFERENCE, newFolderPaths); - editor.apply(); - - scanLibrary(getWritableDatabase()); - } - - public void scanLibrary(SQLiteDatabase database) - { - // Before scanning known folders, go through the game table and remove any entries for which the file itself is missing. - Cursor fileCursor = database.query(TABLE_NAME_GAMES, - null, // Get all columns. - null, // Get all rows. - null, - null, // No grouping. - null, - null); // Order of games is irrelevant. - - // Possibly overly defensive, but ensures that moveToNext() does not skip a row. - fileCursor.moveToPosition(-1); - - while (fileCursor.moveToNext()) - { - String gamePath = fileCursor.getString(GAME_COLUMN_PATH); - File game = new File(gamePath); - - if (!game.exists()) - { - Log.error("[GameDatabase] Game file no longer exists. Removing from the library: " + gamePath); - database.delete(TABLE_NAME_GAMES, - KEY_DB_ID + " = ?", - new String[]{Long.toString(fileCursor.getLong(COLUMN_DB_ID))}); - } - } - - Set allowedExtensions = new HashSet(Arrays.asList( - ".ciso", ".dff", ".dol", ".elf", ".gcm", ".gcz", ".iso", ".tgc", ".wad", ".wbfs")); - - // Iterate through all results of the DB query (i.e. all folders in the library.) - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext); - Set folderPaths = preferences.getStringSet(GAME_FOLDER_PATHS_PREFERENCE, EMPTY_SET); - Set newFolderPaths = new HashSet<>(); - for (String folderPath : folderPaths) - { - File folder = new File(folderPath); - boolean deleteFolder = false; - - Log.info("[GameDatabase] Reading files from library folder: " + folderPath); - - // Iterate through every file in the folder. - File[] children = folder.listFiles(); - - if (children != null) - { - for (File file : children) - { - if (!file.isHidden() && !file.isDirectory()) - { - String filePath = file.getPath(); - - int extensionStart = filePath.lastIndexOf('.'); - if (extensionStart > 0) - { - String fileExtension = filePath.substring(extensionStart); - - // Check that the file has an extension we care about before trying to read out of it. - if (allowedExtensions.contains(fileExtension.toLowerCase())) - { - String name = NativeLibrary.GetTitle(filePath); - - // If the game's title field is empty, use the filename. - if (name.isEmpty()) - { - name = filePath.substring(filePath.lastIndexOf("/") + 1); - } - - String gameId = NativeLibrary.GetGameId(filePath); - - // If the game's ID field is empty, use the filename without extension. - if (gameId.isEmpty()) - { - gameId = filePath.substring(filePath.lastIndexOf("/") + 1, filePath.lastIndexOf(".")); - } - - Platform platform = Platform.fromNativeInt(NativeLibrary.GetPlatform(filePath)); - - ContentValues game = Game.asContentValues(platform, - name, - NativeLibrary.GetDescription(filePath).replace("\n", " "), - NativeLibrary.GetCountry(filePath), - filePath, - gameId, - NativeLibrary.GetCompany(filePath)); - - // Try to update an existing game first. - int rowsMatched = database.update(TABLE_NAME_GAMES, // Which table to update. - game, // The values to fill the row with. - KEY_GAME_ID + " = ?", // The WHERE clause used to find the right row. - new String[]{game.getAsString(KEY_GAME_ID)}); // The ? in WHERE clause is replaced with this, - // which is provided as an array because there - // could potentially be more than one argument. - - // If update fails, insert a new game instead. - if (rowsMatched == 0) - { - Log.verbose("[GameDatabase] Adding game: " + game.getAsString(KEY_GAME_TITLE)); - database.insert(TABLE_NAME_GAMES, null, game); - } - else - { - Log.verbose("[GameDatabase] Updated game: " + game.getAsString(KEY_GAME_TITLE)); - } - } - } - } - } - } - // If the folder is empty because it no longer exists, remove it from the library. - else if (!folder.exists()) - { - Log.error("[GameDatabase] Folder no longer exists. Removing from the library: " + folderPath); - deleteFolder = true; - } - else - { - Log.error("[GameDatabase] Folder contains no games: " + folderPath); - } - - if (!deleteFolder) - { - newFolderPaths.add(folderPath); - } - } - - fileCursor.close(); - database.close(); - - if (folderPaths.size() != newFolderPaths.size()) - { - // One or more folders are being deleted - SharedPreferences.Editor editor = preferences.edit(); - editor.putStringSet(GAME_FOLDER_PATHS_PREFERENCE, newFolderPaths); - editor.apply(); - } - } - - public Observable getGamesForPlatform(final Platform platform) - { - return Observable.create(subscriber -> - { - Log.info("[GameDatabase] Reading games list..."); - - String[] whereArgs = new String[]{Integer.toString(platform.toInt())}; - - SQLiteDatabase database = getReadableDatabase(); - Cursor resultCursor = database.query( - TABLE_NAME_GAMES, - null, - KEY_GAME_PLATFORM + " = ?", - whereArgs, - null, - null, - KEY_GAME_TITLE + " ASC" - ); - - // Pass the result cursor to the consumer. - subscriber.onNext(resultCursor); - - // Tell the consumer we're done; it will unsubscribe implicitly. - subscriber.onCompleted(); - }); - } - - private void execSqlAndLog(SQLiteDatabase database, String sql) - { - Log.verbose("[GameDatabase] Executing SQL: " + sql); - database.execSQL(sql); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFile.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFile.java new file mode 100644 index 0000000000..7e8628d4d2 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFile.java @@ -0,0 +1,34 @@ +package org.dolphinemu.dolphinemu.model; + +import android.os.Environment; + +public class GameFile +{ + private long mPointer; // Do not rename or move without editing the native code + + private GameFile(long pointer) + { + mPointer = pointer; + } + + @Override + public native void finalize(); + + public native int getPlatform(); + public native String getTitle(); + public native String getDescription(); + public native String getCompany(); + public native int getCountry(); + public native String getPath(); + public native String getGameId(); + public native int[] getBanner(); + public native int getBannerWidth(); + public native int getBannerHeight(); + + public String getScreenshotPath() + { + String gameId = getGameId(); + return "file://" + Environment.getExternalStorageDirectory().getPath() + + "/dolphin-emu/ScreenShots/" + gameId + "/" + gameId + "-1.png"; + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFileCache.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFileCache.java new file mode 100644 index 0000000000..6d8cdaa718 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFileCache.java @@ -0,0 +1,89 @@ +package org.dolphinemu.dolphinemu.model; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +public class GameFileCache +{ + private static final String GAME_FOLDER_PATHS_PREFERENCE = "gameFolderPaths"; + private static final Set EMPTY_SET = new HashSet<>(); + + private long mPointer; // Do not rename or move without editing the native code + + public GameFileCache(String path) + { + mPointer = newGameFileCache(path); + } + + private static native long newGameFileCache(String path); + + @Override + public native void finalize(); + + public static void addGameFolder(String path, Context context) + { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + Set folderPaths = preferences.getStringSet(GAME_FOLDER_PATHS_PREFERENCE, EMPTY_SET); + Set newFolderPaths = new HashSet<>(folderPaths); + newFolderPaths.add(path); + SharedPreferences.Editor editor = preferences.edit(); + editor.putStringSet(GAME_FOLDER_PATHS_PREFERENCE, newFolderPaths); + editor.apply(); + } + + private void removeNonExistentGameFolders(Context context) + { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + Set folderPaths = preferences.getStringSet(GAME_FOLDER_PATHS_PREFERENCE, EMPTY_SET); + Set newFolderPaths = new HashSet<>(); + for (String folderPath : folderPaths) + { + File folder = new File(folderPath); + if (folder.exists()) + { + newFolderPaths.add(folderPath); + } + } + + if (folderPaths.size() != newFolderPaths.size()) + { + // One or more folders are being deleted + SharedPreferences.Editor editor = preferences.edit(); + editor.putStringSet(GAME_FOLDER_PATHS_PREFERENCE, newFolderPaths); + editor.apply(); + } + } + + /** + * Scans through the file system and updates the cache to match. + * @return true if the cache was modified + */ + public boolean scanLibrary(Context context) + { + removeNonExistentGameFolders(context); + + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + Set folderPathsSet = preferences.getStringSet(GAME_FOLDER_PATHS_PREFERENCE, EMPTY_SET); + String[] folderPaths = folderPathsSet.toArray(new String[folderPathsSet.size()]); + + boolean cacheChanged = update(folderPaths); + cacheChanged |= updateAdditionalMetadata(); + if (cacheChanged) + { + save(); + } + return cacheChanged; + } + + public native GameFile[] getAllGames(); + public native GameFile addOrGet(String gamePath); + private native boolean update(String[] folderPaths); + private native boolean updateAdditionalMetadata(); + public native boolean load(); + private native boolean save(); +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameProvider.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameProvider.java deleted file mode 100644 index ffb8e5b5ec..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameProvider.java +++ /dev/null @@ -1,139 +0,0 @@ -package org.dolphinemu.dolphinemu.model; - -import android.content.ContentProvider; -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.support.annotation.NonNull; - -import org.dolphinemu.dolphinemu.BuildConfig; -import org.dolphinemu.dolphinemu.utils.Log; - -/** - * Provides an interface allowing Activities to interact with the SQLite database. - * CRUD methods in this class can be called by Activities using getContentResolver(). - */ -public final class GameProvider extends ContentProvider -{ - public static final String REFRESH_LIBRARY = "refresh"; - - public static final String AUTHORITY = "content://" + BuildConfig.APPLICATION_ID + ".provider"; - public static final Uri URI_GAME = Uri.parse(AUTHORITY + "/" + GameDatabase.TABLE_NAME_GAMES + "/"); - public static final Uri URI_REFRESH = Uri.parse(AUTHORITY + "/" + REFRESH_LIBRARY + "/"); - - public static final String MIME_TYPE_FOLDER = "vnd.android.cursor.item/vnd.dolphin.folder"; - public static final String MIME_TYPE_GAME = "vnd.android.cursor.item/vnd.dolphin.game"; - - - private GameDatabase mDbHelper; - - @Override - public boolean onCreate() - { - Log.info("[GameProvider] Creating Content Provider..."); - - mDbHelper = new GameDatabase(getContext()); - - return true; - } - - @Override - public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) - { - Log.info("[GameProvider] Querying URI: " + uri); - - SQLiteDatabase db = mDbHelper.getReadableDatabase(); - - String table = uri.getLastPathSegment(); - - if (table == null) - { - Log.error("[GameProvider] Badly formatted URI: " + uri); - return null; - } - - Cursor cursor = db.query(table, projection, selection, selectionArgs, null, null, sortOrder); - cursor.setNotificationUri(getContext().getContentResolver(), uri); - - return cursor; - } - - @Override - public String getType(@NonNull Uri uri) - { - Log.verbose("[GameProvider] Getting MIME type for URI: " + uri); - String lastSegment = uri.getLastPathSegment(); - - if (lastSegment == null) - { - Log.error("[GameProvider] Badly formatted URI: " + uri); - return null; - } - - if (lastSegment.equals(GameDatabase.TABLE_NAME_GAMES)) - { - return MIME_TYPE_GAME; - } - - Log.error("[GameProvider] Unknown MIME type for URI: " + uri); - return null; - } - - @Override - public Uri insert(@NonNull Uri uri, ContentValues values) - { - Log.info("[GameProvider] Inserting row at URI: " + uri); - - SQLiteDatabase database = mDbHelper.getWritableDatabase(); - String table = uri.getLastPathSegment(); - - long id = -1; - - if (table != null) - { - if (table.equals(REFRESH_LIBRARY)) - { - Log.info("[GameProvider] URI specified table REFRESH_LIBRARY. No insertion necessary; refreshing library contents..."); - mDbHelper.scanLibrary(database); - return uri; - } - - id = database.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_IGNORE); - - // If insertion was successful... - if (id > 0) - { - // Notify the UI that its contents should be refreshed. - getContext().getContentResolver().notifyChange(uri, null); - uri = Uri.withAppendedPath(uri, Long.toString(id)); - } - else - { - Log.error("[GameProvider] Row already exists: " + uri + " id: " + id); - } - } - else - { - Log.error("[GameProvider] Badly formatted URI: " + uri); - } - - database.close(); - - return uri; - } - - @Override - public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) - { - Log.error("[GameProvider] Delete operations unsupported. URI: " + uri); - return 0; - } - - @Override - public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) - { - Log.error("[GameProvider] Update operations unsupported. URI: " + uri); - return 0; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/DirectoryInitializationService.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/DirectoryInitializationService.java index d23b478065..9dde847d35 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/DirectoryInitializationService.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/DirectoryInitializationService.java @@ -32,7 +32,7 @@ import java.util.concurrent.atomic.AtomicBoolean; */ public final class DirectoryInitializationService extends IntentService { - public static final String BROADCAST_ACTION = "org.dolphinemu.dolphinemu.BROADCAST"; + public static final String BROADCAST_ACTION = "org.dolphinemu.dolphinemu.DIRECTORY_INITIALIZATION"; public static final String EXTRA_STATE = "directoryState"; private static volatile DirectoryInitializationState directoryState = null; diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheService.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheService.java new file mode 100644 index 0000000000..c4aa8e5715 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheService.java @@ -0,0 +1,125 @@ +package org.dolphinemu.dolphinemu.services; + +import android.app.IntentService; +import android.content.Context; +import android.content.Intent; +import android.support.v4.content.LocalBroadcastManager; + +import org.dolphinemu.dolphinemu.model.GameFile; +import org.dolphinemu.dolphinemu.model.GameFileCache; +import org.dolphinemu.dolphinemu.ui.platform.Platform; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +/** + * A service that loads game list data on a separate thread. + */ +public final class GameFileCacheService extends IntentService +{ + public static final String BROADCAST_ACTION = "org.dolphinemu.dolphinemu.GAME_FILE_CACHE_UPDATED"; + + 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 AtomicReference gameFiles = new AtomicReference<>(new GameFile[]{}); + + public GameFileCacheService() + { + // Superclass constructor is called to name the thread on which this service executes. + super("GameFileCacheService"); + } + + public static List getGameFilesForPlatform(Platform platform) + { + GameFile[] allGames = gameFiles.get(); + ArrayList platformGames = new ArrayList<>(); + for (GameFile game : allGames) + { + if (Platform.fromNativeInt(game.getPlatform()) == platform) + { + platformGames.add(game); + } + } + return platformGames; + } + + private static void startService(Context context, String action) + { + Intent intent = new Intent(context, GameFileCacheService.class); + intent.setAction(action); + context.startService(intent); + } + + /** + * Asynchronously loads the game file cache from disk without checking + * which games are present on the file system. + */ + public static void startLoad(Context context) + { + startService(context, ACTION_LOAD); + } + + /** + * Asynchronously scans for games in the user's configured folders, + * updating the game file cache with the results. + * If startLoad hasn't been called before this, this has no effect. + */ + public static void startRescan(Context context) + { + startService(context, ACTION_RESCAN); + } + + public static GameFile addOrGet(String gamePath) + { + // The existence of this one function, which is called from one + // single place, forces us to use synchronization in onHandleIntent... + // A bit annoying, but should be good enough for now + synchronized (gameFileCache) + { + return gameFileCache.addOrGet(gamePath); + } + } + + @Override + protected void onHandleIntent(Intent intent) + { + // Load the game list cache if it isn't already loaded, otherwise do nothing + if (ACTION_LOAD.equals(intent.getAction()) && gameFileCache == null) + { + GameFileCache temp = new GameFileCache(getCacheDir() + File.separator + "gamelist.cache"); + synchronized (temp) + { + gameFileCache = temp; + gameFileCache.load(); + updateGameFileArray(); + } + } + + // Rescan the file system and update the game list cache with the results + if (ACTION_RESCAN.equals(intent.getAction()) && gameFileCache != null) + { + synchronized (gameFileCache) + { + if (gameFileCache.scanLibrary(this)) + { + updateGameFileArray(); + } + } + } + } + + private void updateGameFileArray() + { + GameFile[] gameFilesTemp = gameFileCache.getAllGames(); + Arrays.sort(gameFilesTemp, (lhs, rhs) -> lhs.getTitle().compareTo(rhs.getTitle())); + gameFiles.set(gameFilesTemp); + LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(BROADCAST_ACTION)); + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java index 97ad22bd7a..57b9814a75 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java @@ -2,7 +2,6 @@ package org.dolphinemu.dolphinemu.ui.main; import android.content.Intent; import android.content.pm.PackageManager; -import android.database.Cursor; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.design.widget.FloatingActionButton; @@ -18,12 +17,11 @@ import android.widget.Toast; import org.dolphinemu.dolphinemu.R; import org.dolphinemu.dolphinemu.adapters.PlatformPagerAdapter; -import org.dolphinemu.dolphinemu.model.GameProvider; import org.dolphinemu.dolphinemu.services.DirectoryInitializationService; +import org.dolphinemu.dolphinemu.services.GameFileCacheService; import org.dolphinemu.dolphinemu.ui.platform.Platform; import org.dolphinemu.dolphinemu.ui.platform.PlatformGamesView; import org.dolphinemu.dolphinemu.ui.settings.SettingsActivity; -import org.dolphinemu.dolphinemu.utils.AddDirectoryHelper; import org.dolphinemu.dolphinemu.utils.FileBrowserHelper; import org.dolphinemu.dolphinemu.utils.PermissionsHandler; import org.dolphinemu.dolphinemu.utils.StartupHandler; @@ -39,7 +37,7 @@ public final class MainActivity extends AppCompatActivity implements MainView private TabLayout mTabLayout; private FloatingActionButton mFab; - private MainPresenter mPresenter = new MainPresenter(this); + private MainPresenter mPresenter = new MainPresenter(this, this); @Override protected void onCreate(Bundle savedInstanceState) @@ -67,7 +65,11 @@ public final class MainActivity extends AppCompatActivity implements MainView PlatformPagerAdapter platformPagerAdapter = new PlatformPagerAdapter( getSupportFragmentManager(), this); mViewPager.setAdapter(platformPagerAdapter); - } else { + showGames(); + GameFileCacheService.startLoad(this); + } + else + { mViewPager.setVisibility(View.INVISIBLE); } } @@ -76,7 +78,14 @@ public final class MainActivity extends AppCompatActivity implements MainView protected void onResume() { super.onResume(); - mPresenter.addDirIfNeeded(new AddDirectoryHelper(this)); + mPresenter.addDirIfNeeded(this); + } + + @Override + protected void onDestroy() + { + super.onDestroy(); + mPresenter.onDestroy(); } // TODO: Replace with a ButterKnife injection. @@ -106,13 +115,6 @@ public final class MainActivity extends AppCompatActivity implements MainView mToolbar.setSubtitle(version); } - @Override - public void refresh() - { - getContentResolver().insert(GameProvider.URI_REFRESH, null); - refreshAllFragments(); - } - @Override public void refreshFragmentScreenshot(int fragmentPosition) { @@ -138,12 +140,6 @@ public final class MainActivity extends AppCompatActivity implements MainView FileBrowserHelper.openDirectoryPicker(this); } - @Override - public void showGames(Platform platform, Cursor games) - { - // no-op. Handled by PlatformGamesFragment. - } - /** * @param requestCode An int describing whether the Activity that is returning did so successfully. * @param resultCode An int describing what Activity is giving us this callback. @@ -174,12 +170,12 @@ public final class MainActivity extends AppCompatActivity implements MainView case PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION: if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { DirectoryInitializationService.startService(this); - PlatformPagerAdapter platformPagerAdapter = new PlatformPagerAdapter( getSupportFragmentManager(), this); mViewPager.setAdapter(platformPagerAdapter); mTabLayout.setupWithViewPager(mViewPager); mViewPager.setVisibility(View.VISIBLE); + GameFileCacheService.startLoad(this); } else { Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT) .show(); @@ -200,17 +196,17 @@ public final class MainActivity extends AppCompatActivity implements MainView @Override public boolean onOptionsItemSelected(MenuItem item) { - return mPresenter.handleOptionSelection(item.getItemId()); + return mPresenter.handleOptionSelection(item.getItemId(), this); } - private void refreshAllFragments() + public void showGames() { for (Platform platform : Platform.values()) { PlatformGamesView fragment = getPlatformGamesView(platform); if (fragment != null) { - fragment.refresh(); + fragment.showGames(); } } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.java index 4ed97b6ecb..9ae48acb0f 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.java @@ -1,34 +1,57 @@ package org.dolphinemu.dolphinemu.ui.main; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.support.v4.content.LocalBroadcastManager; import org.dolphinemu.dolphinemu.BuildConfig; -import org.dolphinemu.dolphinemu.DolphinApplication; import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.model.GameDatabase; -import org.dolphinemu.dolphinemu.ui.platform.Platform; -import org.dolphinemu.dolphinemu.utils.AddDirectoryHelper; +import org.dolphinemu.dolphinemu.model.GameFileCache; +import org.dolphinemu.dolphinemu.services.GameFileCacheService; import org.dolphinemu.dolphinemu.utils.SettingsFile; -import rx.android.schedulers.AndroidSchedulers; -import rx.schedulers.Schedulers; - public final class MainPresenter { public static final int REQUEST_ADD_DIRECTORY = 1; public static final int REQUEST_EMULATE_GAME = 2; private final MainView mView; + private final Context mContext; + private BroadcastReceiver mBroadcastReceiver = null; private String mDirToAdd; - public MainPresenter(MainView view) + public MainPresenter(MainView view, Context context) { mView = view; + mContext = context; } public void onCreate() { String versionName = BuildConfig.VERSION_NAME; mView.setVersionString(versionName); + + IntentFilter filter = new IntentFilter(); + filter.addAction(GameFileCacheService.BROADCAST_ACTION); + mBroadcastReceiver = new BroadcastReceiver() + { + @Override + public void onReceive(Context context, Intent intent) + { + mView.showGames(); + } + }; + LocalBroadcastManager.getInstance(mContext).registerReceiver(mBroadcastReceiver, filter); + } + + public void onDestroy() + { + if (mBroadcastReceiver != null) + { + LocalBroadcastManager.getInstance(mContext).unregisterReceiver(mBroadcastReceiver); + } } public void onFabClick() @@ -36,7 +59,7 @@ public final class MainPresenter mView.launchFileListActivity(); } - public boolean handleOptionSelection(int itemId) + public boolean handleOptionSelection(int itemId, Context context) { switch (itemId) { @@ -57,9 +80,7 @@ public final class MainPresenter return true; case R.id.menu_refresh: - GameDatabase databaseHelper = DolphinApplication.databaseHelper; - databaseHelper.scanLibrary(databaseHelper.getWritableDatabase()); - mView.refresh(); + GameFileCacheService.startRescan(context); return true; case R.id.button_add_directory: @@ -70,13 +91,13 @@ public final class MainPresenter return false; } - public void addDirIfNeeded(AddDirectoryHelper helper) + public void addDirIfNeeded(Context context) { if (mDirToAdd != null) { - helper.addDirectory(mDirToAdd, mView::refresh); - + GameFileCache.addGameFolder(mDirToAdd, context); mDirToAdd = null; + GameFileCacheService.startRescan(context); } } @@ -89,15 +110,4 @@ public final class MainPresenter { mView.refreshFragmentScreenshot(resultCode); } - - - public void loadGames(final Platform platform) - { - GameDatabase databaseHelper = DolphinApplication.databaseHelper; - - databaseHelper.getGamesForPlatform(platform) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(games -> mView.showGames(platform, games)); - } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainView.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainView.java index fdaef5eefa..cb21b7b084 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainView.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainView.java @@ -2,6 +2,7 @@ package org.dolphinemu.dolphinemu.ui.main; import android.database.Cursor; +import org.dolphinemu.dolphinemu.model.GameFile; import org.dolphinemu.dolphinemu.ui.platform.Platform; /** @@ -19,11 +20,6 @@ public interface MainView */ void setVersionString(String version); - /** - * Tell the view to refresh its contents. - */ - void refresh(); - /** * Tell the view to tell the currently displayed {@link android.support.v4.app.Fragment} * to refresh the screenshot at the given position in its list of games. @@ -38,11 +34,7 @@ public interface MainView void launchFileListActivity(); /** - * To be called when an asynchronous database read completes. Passes the - * result, in this case a {@link Cursor} to the view. - * - * @param platform Which platform to show games for. - * @param games A Cursor containing the games read from the database. + * To be called when the game file cache is updated. */ - void showGames(Platform platform, Cursor games); + void showGames(); } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java index 659ee97d37..10187df402 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java @@ -2,13 +2,10 @@ package org.dolphinemu.dolphinemu.ui.main; import android.content.Intent; import android.content.pm.PackageManager; -import android.database.Cursor; import android.os.Bundle; import android.support.v17.leanback.app.BrowseFragment; import android.support.v17.leanback.app.BrowseSupportFragment; -import android.support.v17.leanback.database.CursorMapper; import android.support.v17.leanback.widget.ArrayObjectAdapter; -import android.support.v17.leanback.widget.CursorObjectAdapter; import android.support.v17.leanback.widget.HeaderItem; import android.support.v17.leanback.widget.ListRow; import android.support.v17.leanback.widget.ListRowPresenter; @@ -21,20 +18,22 @@ import org.dolphinemu.dolphinemu.R; import org.dolphinemu.dolphinemu.activities.EmulationActivity; import org.dolphinemu.dolphinemu.adapters.GameRowPresenter; import org.dolphinemu.dolphinemu.adapters.SettingsRowPresenter; -import org.dolphinemu.dolphinemu.model.Game; +import org.dolphinemu.dolphinemu.model.GameFile; import org.dolphinemu.dolphinemu.model.TvSettingsItem; import org.dolphinemu.dolphinemu.services.DirectoryInitializationService; +import org.dolphinemu.dolphinemu.services.GameFileCacheService; import org.dolphinemu.dolphinemu.ui.platform.Platform; import org.dolphinemu.dolphinemu.ui.settings.SettingsActivity; -import org.dolphinemu.dolphinemu.utils.AddDirectoryHelper; import org.dolphinemu.dolphinemu.utils.FileBrowserHelper; import org.dolphinemu.dolphinemu.utils.PermissionsHandler; import org.dolphinemu.dolphinemu.utils.StartupHandler; import org.dolphinemu.dolphinemu.viewholders.TvGameViewHolder; +import java.util.Collection; + public final class TvMainActivity extends FragmentActivity implements MainView { - private MainPresenter mPresenter = new MainPresenter(this); + private MainPresenter mPresenter = new MainPresenter(this, this); private BrowseSupportFragment mBrowseFragment; @@ -59,7 +58,14 @@ public final class TvMainActivity extends FragmentActivity implements MainView protected void onResume() { super.onResume(); - mPresenter.addDirIfNeeded(new AddDirectoryHelper(this)); + mPresenter.addDirIfNeeded(this); + } + + @Override + protected void onDestroy() + { + super.onDestroy(); + mPresenter.onDestroy(); } void setupUI() { @@ -82,7 +88,7 @@ public final class TvMainActivity extends FragmentActivity implements MainView if (item instanceof TvSettingsItem) { TvSettingsItem settingsItem = (TvSettingsItem) item; - mPresenter.handleOptionSelection(settingsItem.getItemId()); + mPresenter.handleOptionSelection(settingsItem.getItemId(), this); } else { @@ -90,9 +96,7 @@ public final class TvMainActivity extends FragmentActivity implements MainView // Start the emulation activity and send the path of the clicked ISO to it. EmulationActivity.launch(TvMainActivity.this, - holder.path, - holder.title, - holder.screenshotPath, + holder.gameFile, -1, holder.imageScreenshot); } @@ -108,12 +112,6 @@ public final class TvMainActivity extends FragmentActivity implements MainView mBrowseFragment.setTitle(version); } - @Override - public void refresh() - { - recreate(); - } - @Override public void refreshFragmentScreenshot(int fragmentPosition) { @@ -133,15 +131,9 @@ public final class TvMainActivity extends FragmentActivity implements MainView } @Override - public void showGames(Platform platform, Cursor games) + public void showGames() { - ListRow row = buildGamesRow(platform, games); - - // Add row to the adapter only if it is not empty. - if (row != null) - { - mRowsAdapter.add(row); - } + recreate(); } /** @@ -176,7 +168,7 @@ public final class TvMainActivity extends FragmentActivity implements MainView case PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION: if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { DirectoryInitializationService.startService(this); - loadGames(); + GameFileCacheService.startLoad(this); } else { Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT) .show(); @@ -194,48 +186,36 @@ public final class TvMainActivity extends FragmentActivity implements MainView if (PermissionsHandler.hasWriteAccess(this)) { - loadGames(); + GameFileCacheService.startLoad(this); } mRowsAdapter.add(buildSettingsRow()); + for (Platform platform : Platform.values()) + { + ListRow row = buildGamesRow(platform, GameFileCacheService.getGameFilesForPlatform(platform)); + + // Add row to the adapter only if it is not empty. + if (row != null) + { + mRowsAdapter.add(row); + } + } + mBrowseFragment.setAdapter(mRowsAdapter); } - private void loadGames() { - for (Platform platform : Platform.values()) { - mPresenter.loadGames(platform); - } - } - - private ListRow buildGamesRow(Platform platform, Cursor games) + private ListRow buildGamesRow(Platform platform, Collection gameFiles) { - // Create an adapter for this row. - CursorObjectAdapter row = new CursorObjectAdapter(new GameRowPresenter()); - - // If cursor is empty, don't return a Row. - if (!games.moveToFirst()) + // If there are no games, don't return a Row. + if (gameFiles.size() == 0) { return null; } - row.changeCursor(games); - row.setMapper(new CursorMapper() - { - @Override - protected void bindColumns(Cursor cursor) - { - // No-op? Not sure what this does. - } - - @Override - protected Object bind(Cursor cursor) - { - return Game.fromCursor(cursor); - } - }); - - String headerName = platform.getHeaderName(); + // Create an adapter for this row. + ArrayObjectAdapter row = new ArrayObjectAdapter(new GameRowPresenter()); + row.addAll(0, gameFiles); // Create a header for this row. HeaderItem header = new HeaderItem(platform.toInt(), platform.getHeaderName()); diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/Platform.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/Platform.java index b161a1cde4..64f1cc71cd 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/Platform.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/Platform.java @@ -23,11 +23,9 @@ public enum Platform public static Platform fromNativeInt(int i) { - // If the game's platform field is empty, file under Wiiware. // TODO Something less dum - if (i == -1) { - return Platform.WIIWARE; - } - return values()[i]; + // TODO: Proper support for DOL and ELF files + boolean in_range = i >= 0 && i < values().length; + return values()[in_range ? i : WIIWARE.value]; } public static Platform fromPosition(int position) @@ -44,4 +42,4 @@ public enum Platform { return headerName; } -} \ No newline at end of file +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesFragment.java index 3325a1ca23..7d15609055 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesFragment.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesFragment.java @@ -1,6 +1,5 @@ package org.dolphinemu.dolphinemu.ui.platform; -import android.database.Cursor; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; @@ -12,13 +11,15 @@ import android.view.ViewGroup; import org.dolphinemu.dolphinemu.R; import org.dolphinemu.dolphinemu.adapters.GameAdapter; +import org.dolphinemu.dolphinemu.model.GameFile; +import org.dolphinemu.dolphinemu.services.GameFileCacheService; + +import java.util.List; public final class PlatformGamesFragment extends Fragment implements PlatformGamesView { private static final String ARG_PLATFORM = "platform"; - private PlatformGamesPresenter mPresenter = new PlatformGamesPresenter(this); - private GameAdapter mAdapter; private RecyclerView mRecyclerView; @@ -37,8 +38,6 @@ public final class PlatformGamesFragment extends Fragment implements PlatformGam public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - - mPresenter.onCreate((Platform) getArguments().getSerializable(ARG_PLATFORM)); } @Nullable @@ -49,8 +48,6 @@ public final class PlatformGamesFragment extends Fragment implements PlatformGam findViews(rootView); - mPresenter.onCreateView(); - return rootView; } @@ -65,6 +62,8 @@ public final class PlatformGamesFragment extends Fragment implements PlatformGam mRecyclerView.setAdapter(mAdapter); mRecyclerView.addItemDecoration(new GameAdapter.SpacesItemDecoration(8)); + + showGames(); } @Override @@ -73,12 +72,6 @@ public final class PlatformGamesFragment extends Fragment implements PlatformGam mAdapter.notifyItemChanged(position); } - @Override - public void refresh() - { - mPresenter.refresh(); - } - @Override public void onItemClick(String gameId) { @@ -86,11 +79,12 @@ public final class PlatformGamesFragment extends Fragment implements PlatformGam } @Override - public void showGames(Cursor games) + public void showGames() { if (mAdapter != null) { - mAdapter.swapCursor(games); + Platform platform = (Platform) getArguments().getSerializable(ARG_PLATFORM); + mAdapter.swapDataSet(GameFileCacheService.getGameFilesForPlatform(platform)); } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesPresenter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesPresenter.java deleted file mode 100644 index 4c6782dd5f..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesPresenter.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.dolphinemu.dolphinemu.ui.platform; - - -import android.database.Cursor; - -import org.dolphinemu.dolphinemu.DolphinApplication; -import org.dolphinemu.dolphinemu.model.GameDatabase; -import org.dolphinemu.dolphinemu.utils.Log; - -import rx.android.schedulers.AndroidSchedulers; -import rx.functions.Action1; -import rx.schedulers.Schedulers; - -public final class PlatformGamesPresenter -{ - private final PlatformGamesView mView; - - private Platform mPlatform; - - public PlatformGamesPresenter(PlatformGamesView view) - { - mView = view; - } - - public void onCreate(Platform platform) - { - mPlatform = platform; - } - - public void onCreateView() - { - loadGames(); - } - - public void refresh() - { - Log.debug("[PlatformGamesPresenter] " + mPlatform + ": Refreshing..."); - loadGames(); - } - - private void loadGames() - { - Log.debug("[PlatformGamesPresenter] " + mPlatform + ": Loading games..."); - - GameDatabase databaseHelper = DolphinApplication.databaseHelper; - - databaseHelper.getGamesForPlatform(mPlatform) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(games -> - { - Log.debug("[PlatformGamesPresenter] " + mPlatform + ": Load finished, swapping cursor..."); - - mView.showGames(games); - }); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesView.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesView.java index 361d16dfbc..b584b8090f 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesView.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesView.java @@ -1,17 +1,14 @@ package org.dolphinemu.dolphinemu.ui.platform; -import android.database.Cursor; +import org.dolphinemu.dolphinemu.model.GameFile; + +import java.util.List; /** * Abstraction for a screen representing a single platform's games. */ public interface PlatformGamesView { - /** - * Tell the view to refresh its contents. - */ - void refresh(); - /** * Tell the view that a certain game's screenshot has been updated, * and should be redrawn on-screen. @@ -29,10 +26,7 @@ public interface PlatformGamesView void onItemClick(String gameId); /** - * To be called when an asynchronous database read completes. Passes the - * result, in this case a {@link Cursor}, to the view. - * - * @param games A Cursor containing the games read from the database. + * To be called when the game file cache is updated. */ - void showGames(Cursor games); + void showGames(); } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/AddDirectoryHelper.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/AddDirectoryHelper.java deleted file mode 100644 index 7678779707..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/AddDirectoryHelper.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.dolphinemu.dolphinemu.utils; - -import android.content.Context; -import android.os.AsyncTask; - -import org.dolphinemu.dolphinemu.DolphinApplication; -import org.dolphinemu.dolphinemu.model.GameDatabase; - -public class AddDirectoryHelper -{ - private Context mContext; - - public interface AddDirectoryListener - { - void onDirectoryAdded(); - } - - public AddDirectoryHelper(Context context) - { - this.mContext = context; - } - - public void addDirectory(String dir, AddDirectoryListener addDirectoryListener) - { - new AsyncTask() - { - @Override - protected Void doInBackground(String... params) - { - for (String path : params) - { - DolphinApplication.databaseHelper.addGameFolder(path); - } - return null; - } - - @Override - protected void onPostExecute(Void result) - { - addDirectoryListener.onDirectoryAdded(); - } - }.execute(dir); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GameBannerRequestHandler.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GameBannerRequestHandler.java index ef95cd7712..b8afcf6a52 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GameBannerRequestHandler.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GameBannerRequestHandler.java @@ -7,22 +7,28 @@ import com.squareup.picasso.Request; import com.squareup.picasso.RequestHandler; import org.dolphinemu.dolphinemu.NativeLibrary; +import org.dolphinemu.dolphinemu.model.GameFile; import java.io.IOException; import java.nio.IntBuffer; public class GameBannerRequestHandler extends RequestHandler { + GameFile mGameFile; + + public GameBannerRequestHandler(GameFile gameFile) + { + mGameFile = gameFile; + } @Override public boolean canHandleRequest(Request data) { - return "iso".equals(data.uri.getScheme()); + return true; } @Override - public Result load(Request request, int networkPolicy) throws IOException { - String url = request.uri.getHost() + request.uri.getPath(); - int[] vector = NativeLibrary.GetBanner(url); - int width = 96; - int height = 32; + public Result load(Request request, int networkPolicy) { + int[] vector = mGameFile.getBanner(); + int width = mGameFile.getBannerWidth(); + int height = mGameFile.getBannerHeight(); Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); bitmap.setPixels(vector, 0, width, 0, 0, width, height); return new Result(bitmap, Picasso.LoadedFrom.DISK); diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PicassoUtils.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PicassoUtils.java index 36a00d884d..4630a3cddc 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PicassoUtils.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PicassoUtils.java @@ -7,17 +7,18 @@ import android.widget.ImageView; import com.squareup.picasso.Picasso; import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.model.GameFile; import java.io.File; import java.net.URI; public class PicassoUtils { - public static void loadGameBanner(ImageView imageView, String screenshotPath, String gamePath) { - File file = new File(URI.create(screenshotPath)); - if (file.exists()) { + public static void loadGameBanner(ImageView imageView, GameFile gameFile) { + File screenshotFile = new File(URI.create(gameFile.getScreenshotPath())); + if (screenshotFile.exists()) { // Fill in the view contents. Picasso.with(imageView.getContext()) - .load(screenshotPath) + .load(gameFile.getScreenshotPath()) .fit() .centerCrop() .noFade() @@ -27,11 +28,11 @@ public class PicassoUtils { .into(imageView); } else { Picasso picassoInstance = new Picasso.Builder(imageView.getContext()) - .addRequestHandler(new GameBannerRequestHandler()) + .addRequestHandler(new GameBannerRequestHandler(gameFile)) .build(); picassoInstance - .load(Uri.parse("iso:/" + gamePath)) + .load(Uri.parse("iso:/" + gameFile.getPath())) .fit() .noFade() .noPlaceholder() diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/GameViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/GameViewHolder.java index 592a2ecf32..e993ba7961 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/GameViewHolder.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/GameViewHolder.java @@ -6,6 +6,7 @@ import android.widget.ImageView; import android.widget.TextView; import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.model.GameFile; /** * A simple class that stores references to views so that the GameAdapter doesn't need to @@ -17,15 +18,7 @@ public class GameViewHolder extends RecyclerView.ViewHolder public TextView textGameTitle; public TextView textCompany; - public String gameId; - - // TODO Not need any of this stuff. Currently only the properties dialog needs it. - public String path; - public String title; - public String description; - public int country; - public String company; - public String screenshotPath; + public GameFile gameFile; public GameViewHolder(View itemView) { @@ -33,8 +26,8 @@ public class GameViewHolder extends RecyclerView.ViewHolder itemView.setTag(this); - imageScreenshot = (ImageView) itemView.findViewById(R.id.image_game_screen); - textGameTitle = (TextView) itemView.findViewById(R.id.text_game_title); - textCompany = (TextView) itemView.findViewById(R.id.text_company); + imageScreenshot = itemView.findViewById(R.id.image_game_screen); + textGameTitle = itemView.findViewById(R.id.text_game_title); + textCompany = itemView.findViewById(R.id.text_company); } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvGameViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvGameViewHolder.java index dbd018be78..54615c6710 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvGameViewHolder.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvGameViewHolder.java @@ -5,6 +5,8 @@ import android.support.v17.leanback.widget.Presenter; import android.view.View; import android.widget.ImageView; +import org.dolphinemu.dolphinemu.model.GameFile; + /** * A simple class that stores references to views so that the GameAdapter doesn't need to * keep calling findViewById(), which is expensive. @@ -15,15 +17,7 @@ public final class TvGameViewHolder extends Presenter.ViewHolder public ImageView imageScreenshot; - public String gameId; - - // TODO Not need any of this stuff. Currently only the properties dialog needs it. - public String path; - public String title; - public String description; - public int country; - public String company; - public String screenshotPath; + public GameFile gameFile; public TvGameViewHolder(View itemView) { diff --git a/Source/Android/app/src/main/res/layout/dialog_game_details.xml b/Source/Android/app/src/main/res/layout/dialog_game_details.xml index efd17e74cb..61cc212f55 100644 --- a/Source/Android/app/src/main/res/layout/dialog_game_details.xml +++ b/Source/Android/app/src/main/res/layout/dialog_game_details.xml @@ -50,7 +50,7 @@ tools:text="Rhythm Heaven Fever"/> @@ -96,13 +96,13 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignBottom="@+id/icon_country" - android:layout_alignStart="@+id/text_company" + android:layout_alignStart="@+id/text_description" android:layout_alignTop="@+id/icon_country" android:gravity="center_vertical" tools:text="United States"/> + +#include + +std::string GetJString(JNIEnv* env, jstring jstr) +{ + std::string result = ""; + if (!jstr) + return result; + + const char* s = env->GetStringUTFChars(jstr, nullptr); + result = s; + env->ReleaseStringUTFChars(jstr, s); + return result; +} + +jstring ToJString(JNIEnv* env, const std::string& str) +{ + return env->NewStringUTF(str.c_str()); +} diff --git a/Source/Android/jni/AndroidCommon/AndroidCommon.h b/Source/Android/jni/AndroidCommon/AndroidCommon.h new file mode 100644 index 0000000000..25ea59ed30 --- /dev/null +++ b/Source/Android/jni/AndroidCommon/AndroidCommon.h @@ -0,0 +1,12 @@ +// Copyright 2018 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include + +#include + +std::string GetJString(JNIEnv* env, jstring jstr); +jstring ToJString(JNIEnv* env, const std::string& str); diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp new file mode 100644 index 0000000000..2c457d6c9d --- /dev/null +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -0,0 +1,110 @@ +// Copyright 2018 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include "jni/AndroidCommon/IDCache.h" + +#include + +static constexpr jint JNI_VERSION = JNI_VERSION_1_6; + +static JavaVM* s_java_vm; + +static jclass s_native_library_class; +static jmethodID s_display_alert_msg; + +static jclass s_game_file_class; +static jfieldID s_game_file_pointer; +static jmethodID s_game_file_constructor; + +static jclass s_game_file_cache_class; +static jfieldID s_game_file_cache_pointer; + +namespace IDCache +{ +JavaVM* GetJavaVM() +{ + return s_java_vm; +} + +jclass GetNativeLibraryClass() +{ + return s_native_library_class; +} + +jmethodID GetDisplayAlertMsg() +{ + return s_display_alert_msg; +} + +jclass GetGameFileClass() +{ + return s_game_file_class; +} + +jfieldID GetGameFilePointer() +{ + return s_game_file_pointer; +} + +jmethodID GetGameFileConstructor() +{ + return s_game_file_constructor; +} + +jclass GetGameFileCacheClass() +{ + return s_game_file_cache_class; +} + +jfieldID GetGameFileCachePointer() +{ + return s_game_file_cache_pointer; +} + +} // namespace IDCache + +#ifdef __cplusplus +extern "C" { +#endif + +jint JNI_OnLoad(JavaVM* vm, void* reserved) +{ + s_java_vm = vm; + + JNIEnv* env; + if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION) != JNI_OK) + return JNI_ERR; + + const jclass native_library_class = env->FindClass("org/dolphinemu/dolphinemu/NativeLibrary"); + s_native_library_class = reinterpret_cast(env->NewGlobalRef(native_library_class)); + s_display_alert_msg = env->GetStaticMethodID(s_native_library_class, "displayAlertMsg", + "(Ljava/lang/String;Ljava/lang/String;Z)Z"); + + const jclass game_file_class = env->FindClass("org/dolphinemu/dolphinemu/model/GameFile"); + s_game_file_class = reinterpret_cast(env->NewGlobalRef(game_file_class)); + s_game_file_pointer = env->GetFieldID(game_file_class, "mPointer", "J"); + s_game_file_constructor = env->GetMethodID(game_file_class, "", "(J)V"); + + const jclass game_file_cache_class = + env->FindClass("org/dolphinemu/dolphinemu/model/GameFileCache"); + s_game_file_cache_class = reinterpret_cast(env->NewGlobalRef(game_file_cache_class)); + s_game_file_cache_pointer = env->GetFieldID(game_file_cache_class, "mPointer", "J"); + + return JNI_VERSION; +} + +void JNI_OnUnload(JavaVM* vm, void* reserved) +{ + JNIEnv* env; + if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION) != JNI_OK) + return; + + env->DeleteGlobalRef(s_native_library_class); + env->DeleteGlobalRef(s_game_file_class); + env->DeleteGlobalRef(s_game_file_cache_class); +} + +#ifdef __cplusplus +} +#endif diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h new file mode 100644 index 0000000000..584b0634ab --- /dev/null +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -0,0 +1,23 @@ +// Copyright 2018 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include + +namespace IDCache +{ +JavaVM* GetJavaVM(); + +jclass GetNativeLibraryClass(); +jmethodID GetDisplayAlertMsg(); + +jclass GetGameFileClass(); +jfieldID GetGameFilePointer(); +jmethodID GetGameFileConstructor(); + +jclass GetGameFileCacheClass(); +jfieldID GetGameFileCachePointer(); + +} // namespace IDCache diff --git a/Source/Android/jni/CMakeLists.txt b/Source/Android/jni/CMakeLists.txt index 69b383019b..848da01823 100644 --- a/Source/Android/jni/CMakeLists.txt +++ b/Source/Android/jni/CMakeLists.txt @@ -1,4 +1,8 @@ add_library(main SHARED + AndroidCommon/AndroidCommon.cpp + AndroidCommon/IDCache.cpp + GameList/GameFile.cpp + GameList/GameFileCache.cpp ButtonManager.cpp MainAndroid.cpp ) diff --git a/Source/Android/jni/GameList/GameFile.cpp b/Source/Android/jni/GameList/GameFile.cpp new file mode 100644 index 0000000000..b2cf26cd52 --- /dev/null +++ b/Source/Android/jni/GameList/GameFile.cpp @@ -0,0 +1,140 @@ +// Copyright 2018 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include "jni/GameList/GameFile.h" + +#include +#include +#include + +#include + +#include "DiscIO/Enums.h" +#include "UICommon/GameFile.h" +#include "jni/AndroidCommon/AndroidCommon.h" +#include "jni/AndroidCommon/IDCache.h" + +static std::shared_ptr* GetPointer(JNIEnv* env, jobject obj) +{ + return reinterpret_cast*>( + env->GetLongField(obj, IDCache::GetGameFilePointer())); +} + +static std::shared_ptr& GetRef(JNIEnv* env, jobject obj) +{ + return *GetPointer(env, obj); +} + +jobject GameFileToJava(JNIEnv* env, std::shared_ptr game_file) +{ + if (!game_file) + return nullptr; + + return env->NewObject( + IDCache::GetGameFileClass(), IDCache::GetGameFileConstructor(), + reinterpret_cast(new std::shared_ptr(std::move(game_file)))); +} + +#ifdef __cplusplus +extern "C" { +#endif + +JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_finalize(JNIEnv* env, + jobject obj); +JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getPlatform(JNIEnv* env, + jobject obj); +JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getTitle(JNIEnv* env, + jobject obj); +JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getDescription(JNIEnv* env, + jobject obj); +JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getCompany(JNIEnv* env, + jobject obj); +JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getCountry(JNIEnv* env, + jobject obj); +JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getPath(JNIEnv* env, + jobject obj); +JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getGameId(JNIEnv* env, + jobject obj); +JNIEXPORT jintArray JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getBanner(JNIEnv* env, + jobject obj); +JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getBannerWidth(JNIEnv* env, + jobject obj); +JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getBannerHeight(JNIEnv* env, + jobject obj); + +JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_finalize(JNIEnv* env, + jobject obj) +{ + delete GetPointer(env, obj); +} + +JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getPlatform(JNIEnv* env, + jobject obj) +{ + return static_cast(GetRef(env, obj)->GetPlatform()); +} + +JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getTitle(JNIEnv* env, + jobject obj) +{ + return ToJString(env, GetRef(env, obj)->GetName()); +} + +JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getDescription(JNIEnv* env, + jobject obj) +{ + return ToJString(env, GetRef(env, obj)->GetDescription()); +} + +JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getCompany(JNIEnv* env, + jobject obj) +{ + return ToJString(env, DiscIO::GetCompanyFromID(GetRef(env, obj)->GetMakerID())); +} + +JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getCountry(JNIEnv* env, + jobject obj) +{ + return static_cast(GetRef(env, obj)->GetCountry()); +} + +JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getPath(JNIEnv* env, + jobject obj) +{ + return ToJString(env, GetRef(env, obj)->GetFilePath()); +} + +JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getGameId(JNIEnv* env, + jobject obj) +{ + return ToJString(env, GetRef(env, obj)->GetGameID()); +} + +JNIEXPORT jintArray JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getBanner(JNIEnv* env, + jobject obj) +{ + const std::vector& buffer = GetRef(env, obj)->GetBannerImage().buffer; + const jsize size = static_cast(buffer.size()); + const jintArray out_array = env->NewIntArray(size); + if (!out_array) + return nullptr; + env->SetIntArrayRegion(out_array, 0, size, reinterpret_cast(buffer.data())); + return out_array; +} + +JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getBannerWidth(JNIEnv* env, + jobject obj) +{ + return static_cast(GetRef(env, obj)->GetBannerImage().width); +} + +JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getBannerHeight(JNIEnv* env, + jobject obj) +{ + return static_cast(GetRef(env, obj)->GetBannerImage().height); +} + +#ifdef __cplusplus +} +#endif diff --git a/Source/Android/jni/GameList/GameFile.h b/Source/Android/jni/GameList/GameFile.h new file mode 100644 index 0000000000..9bf4f1cce4 --- /dev/null +++ b/Source/Android/jni/GameList/GameFile.h @@ -0,0 +1,17 @@ +// Copyright 2018 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include +#include + +#include + +namespace UICommon +{ +class GameFile; +} + +jobject GameFileToJava(JNIEnv* env, std::shared_ptr game_file); diff --git a/Source/Android/jni/GameList/GameFileCache.cpp b/Source/Android/jni/GameList/GameFileCache.cpp new file mode 100644 index 0000000000..4b3abf8c2f --- /dev/null +++ b/Source/Android/jni/GameList/GameFileCache.cpp @@ -0,0 +1,121 @@ +// Copyright 2018 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include +#include + +#include + +#include "UICommon/GameFileCache.h" +#include "jni/AndroidCommon/AndroidCommon.h" +#include "jni/AndroidCommon/IDCache.h" +#include "jni/GameList/GameFile.h" + +namespace UICommon +{ +class GameFile; +} + +static UICommon::GameFileCache* GetPointer(JNIEnv* env, jobject obj) +{ + return reinterpret_cast( + env->GetLongField(obj, IDCache::GetGameFileCachePointer())); +} + +#ifdef __cplusplus +extern "C" { +#endif + +JNIEXPORT jlong JNICALL Java_org_dolphinemu_dolphinemu_model_GameFileCache_newGameFileCache( + JNIEnv* env, jobject obj, jstring path); +JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_model_GameFileCache_finalize(JNIEnv* env, + jobject obj); +JNIEXPORT jobjectArray JNICALL +Java_org_dolphinemu_dolphinemu_model_GameFileCache_getAllGames(JNIEnv* env, jobject obj); +JNIEXPORT jobject JNICALL Java_org_dolphinemu_dolphinemu_model_GameFileCache_addOrGet(JNIEnv* env, + jobject obj, + jstring path); +JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_model_GameFileCache_update( + JNIEnv* env, jobject obj, jobjectArray folder_paths); +JNIEXPORT jboolean JNICALL +Java_org_dolphinemu_dolphinemu_model_GameFileCache_updateAdditionalMetadata(JNIEnv* env, + jobject obj); +JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_model_GameFileCache_load(JNIEnv* env, + jobject obj); +JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_model_GameFileCache_save(JNIEnv* env, + jobject obj); + +JNIEXPORT jlong JNICALL Java_org_dolphinemu_dolphinemu_model_GameFileCache_newGameFileCache( + JNIEnv* env, jobject obj, jstring path) +{ + return reinterpret_cast(new UICommon::GameFileCache(GetJString(env, path))); +} + +JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_model_GameFileCache_finalize(JNIEnv* env, + jobject obj) +{ + delete GetPointer(env, obj); +} + +JNIEXPORT jobjectArray JNICALL +Java_org_dolphinemu_dolphinemu_model_GameFileCache_getAllGames(JNIEnv* env, jobject obj) +{ + const UICommon::GameFileCache* ptr = GetPointer(env, obj); + const jobjectArray array = + env->NewObjectArray(static_cast(ptr->GetSize()), IDCache::GetGameFileClass(), nullptr); + jsize i = 0; + GetPointer(env, obj)->ForEach([env, array, &i](const auto& game_file) { + env->SetObjectArrayElement(array, i++, GameFileToJava(env, game_file)); + }); + return array; +} + +JNIEXPORT jobject JNICALL Java_org_dolphinemu_dolphinemu_model_GameFileCache_addOrGet(JNIEnv* env, + jobject obj, + jstring path) +{ + bool cache_changed = false; + return GameFileToJava(env, GetPointer(env, obj)->AddOrGet(GetJString(env, path), &cache_changed)); +} + +JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_model_GameFileCache_update( + JNIEnv* env, jobject obj, jobjectArray folder_paths) +{ + jsize size = env->GetArrayLength(folder_paths); + + std::vector folder_paths_vector; + folder_paths_vector.reserve(size); + + for (jsize i = 0; i < size; ++i) + { + const jstring path = reinterpret_cast(env->GetObjectArrayElement(folder_paths, i)); + folder_paths_vector.push_back(GetJString(env, path)); + env->DeleteLocalRef(path); + } + + return GetPointer(env, obj)->Update(UICommon::FindAllGamePaths(folder_paths_vector, false)); +} + +JNIEXPORT jboolean JNICALL +Java_org_dolphinemu_dolphinemu_model_GameFileCache_updateAdditionalMetadata(JNIEnv* env, + jobject obj) +{ + return GetPointer(env, obj)->UpdateAdditionalMetadata(); +} + +JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_model_GameFileCache_load(JNIEnv* env, + jobject obj) +{ + return GetPointer(env, obj)->Load(); +} + +JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_model_GameFileCache_save(JNIEnv* env, + jobject obj) +{ + return GetPointer(env, obj)->Save(); +} + +#ifdef __cplusplus +} +#endif diff --git a/Source/Android/jni/MainAndroid.cpp b/Source/Android/jni/MainAndroid.cpp index 62d31580e7..730175e340 100644 --- a/Source/Android/jni/MainAndroid.cpp +++ b/Source/Android/jni/MainAndroid.cpp @@ -16,8 +16,6 @@ #include #include -#include "ButtonManager.h" - #include "Common/CPUDetect.h" #include "Common/CommonPaths.h" #include "Common/CommonTypes.h" @@ -51,16 +49,15 @@ #include "VideoCommon/RenderBase.h" #include "VideoCommon/VideoBackendBase.h" -#define DOLPHIN_TAG "DolphinEmuNative" - -JavaVM* g_java_vm; +#include "jni/AndroidCommon/AndroidCommon.h" +#include "jni/AndroidCommon/IDCache.h" +#include "jni/ButtonManager.h" namespace { -ANativeWindow* s_surf; +static constexpr char DOLPHIN_TAG[] = "DolphinEmuNative"; -jclass s_jni_class; -jmethodID s_jni_method_alert; +ANativeWindow* s_surf; // The Core only supports using a single Host thread. // If multiple threads want to call host functions then they need to queue @@ -70,16 +67,6 @@ Common::Event s_update_main_frame_event; bool s_have_wm_user_stop = false; } // Anonymous namespace -/* - * Cache the JavaVM so that we can call into it later. - */ -jint JNI_OnLoad(JavaVM* vm, void* reserved) -{ - g_java_vm = vm; - - return JNI_VERSION_1_6; -} - void Host_NotifyMapLoaded() { } @@ -156,229 +143,19 @@ static bool MsgAlert(const char* caption, const char* text, bool yes_no, MsgType // Associate the current Thread with the Java VM. JNIEnv* env; - g_java_vm->AttachCurrentThread(&env, NULL); + IDCache::GetJavaVM()->AttachCurrentThread(&env, nullptr); // Execute the Java method. - jboolean result = - env->CallStaticBooleanMethod(s_jni_class, s_jni_method_alert, env->NewStringUTF(caption), - env->NewStringUTF(text), yes_no ? JNI_TRUE : JNI_FALSE); + jboolean result = env->CallStaticBooleanMethod( + IDCache::GetNativeLibraryClass(), IDCache::GetDisplayAlertMsg(), ToJString(env, caption), + ToJString(env, text), yes_no ? JNI_TRUE : JNI_FALSE); // Must be called before the current thread exits; might as well do it here. - g_java_vm->DetachCurrentThread(); + IDCache::GetJavaVM()->DetachCurrentThread(); return result != JNI_FALSE; } -#define DVD_BANNER_WIDTH 96 -#define DVD_BANNER_HEIGHT 32 - -static inline u32 Average32(u32 a, u32 b) -{ - return ((a >> 1) & 0x7f7f7f7f) + ((b >> 1) & 0x7f7f7f7f); -} - -static inline u32 GetPixel(u32* buffer, unsigned int x, unsigned int y) -{ - // thanks to unsignedness, these also check for <0 automatically. - if (x > 191) - return 0; - if (y > 63) - return 0; - return buffer[y * 192 + x]; -} - -static bool LoadBanner(std::string filename, u32* Banner) -{ - std::unique_ptr pVolume(DiscIO::CreateVolumeFromFilename(filename)); - - if (pVolume != nullptr) - { - u32 Width, Height; - std::vector BannerVec = pVolume->GetBanner(&Width, &Height); - // This code (along with above inlines) is moved from - // elsewhere. Someone who knows anything about Android - // please get rid of it and use proper high-resolution - // images. - if (Height == 64 && Width == 192) - { - u32* Buffer = &BannerVec[0]; - for (int y = 0; y < 32; y++) - { - for (int x = 0; x < 96; x++) - { - // simplified plus-shaped "gaussian" - u32 surround = Average32( - Average32(GetPixel(Buffer, x * 2 - 1, y * 2), GetPixel(Buffer, x * 2 + 1, y * 2)), - Average32(GetPixel(Buffer, x * 2, y * 2 - 1), GetPixel(Buffer, x * 2, y * 2 + 1))); - Banner[y * 96 + x] = Average32(GetPixel(Buffer, x * 2, y * 2), surround); - } - } - return true; - } - else if (Height == 32 && Width == 96) - { - memcpy(Banner, &BannerVec[0], 96 * 32 * 4); - return true; - } - } - - return false; -} - -static int GetCountry(std::string filename) -{ - std::unique_ptr pVolume(DiscIO::CreateVolumeFromFilename(filename)); - - if (pVolume != nullptr) - { - int country = static_cast(pVolume->GetCountry()); - - __android_log_print(ANDROID_LOG_INFO, DOLPHIN_TAG, "Country Code: %i", country); - - return country; - } - - return static_cast(DiscIO::Country::Unknown); -} - -static int GetPlatform(std::string filename) -{ - std::unique_ptr pVolume(DiscIO::CreateVolumeFromFilename(filename)); - - if (pVolume != nullptr) - { - switch (pVolume->GetVolumeType()) - { - case DiscIO::Platform::GameCubeDisc: - __android_log_print(ANDROID_LOG_INFO, DOLPHIN_TAG, "Volume is a GameCube disc."); - return 0; - case DiscIO::Platform::WiiDisc: - __android_log_print(ANDROID_LOG_INFO, DOLPHIN_TAG, "Volume is a Wii disc."); - return 1; - case DiscIO::Platform::WiiWAD: - __android_log_print(ANDROID_LOG_INFO, DOLPHIN_TAG, "Volume is a Wii WAD."); - return 2; - } - } - - return -1; -} - -static std::string GetTitle(std::string filename) -{ - __android_log_print(ANDROID_LOG_WARN, DOLPHIN_TAG, "Getting Title for file: %s", - filename.c_str()); - - std::unique_ptr pVolume(DiscIO::CreateVolumeFromFilename(filename)); - - if (pVolume != nullptr) - { - std::map titles = pVolume->GetLongNames(); - if (titles.empty()) - titles = pVolume->GetShortNames(); - - auto end = titles.end(); - - // English tends to be a good fallback when the requested language isn't available - // if (language != DiscIO::Language::English) { - auto it = titles.find(DiscIO::Language::English); - if (it != end) - return it->second; - //} - - // If English isn't available either, just pick something - if (!titles.empty()) - return titles.cbegin()->second; - - // No usable name, return filename (better than nothing) - std::string name; - SplitPath(filename, nullptr, &name, nullptr); - return name; - } - - return std::string(""); -} - -static std::string GetDescription(std::string filename) -{ - __android_log_print(ANDROID_LOG_WARN, DOLPHIN_TAG, "Getting Description for file: %s", - filename.c_str()); - - std::unique_ptr volume(DiscIO::CreateVolumeFromFilename(filename)); - - if (volume != nullptr) - { - std::map descriptions = volume->GetDescriptions(); - - auto end = descriptions.end(); - - // English tends to be a good fallback when the requested language isn't available - // if (language != DiscIO::Language::English) { - auto it = descriptions.find(DiscIO::Language::English); - if (it != end) - return it->second; - //} - - // If English isn't available either, just pick something - if (!descriptions.empty()) - return descriptions.cbegin()->second; - } - - return std::string(); -} - -static std::string GetGameId(std::string filename) -{ - __android_log_print(ANDROID_LOG_WARN, DOLPHIN_TAG, "Getting ID for file: %s", filename.c_str()); - - std::unique_ptr volume(DiscIO::CreateVolumeFromFilename(filename)); - if (volume == nullptr) - return std::string(); - - std::string id = volume->GetGameID(); - __android_log_print(ANDROID_LOG_INFO, DOLPHIN_TAG, "Game ID: %s", id.c_str()); - return id; -} - -static std::string GetCompany(std::string filename) -{ - __android_log_print(ANDROID_LOG_WARN, DOLPHIN_TAG, "Getting Company for file: %s", - filename.c_str()); - - std::unique_ptr volume(DiscIO::CreateVolumeFromFilename(filename)); - if (volume == nullptr) - return std::string(); - - std::string company = DiscIO::GetCompanyFromID(volume->GetMakerID()); - __android_log_print(ANDROID_LOG_INFO, DOLPHIN_TAG, "Company: %s", company.c_str()); - return company; -} - -static u64 GetFileSize(std::string filename) -{ - __android_log_print(ANDROID_LOG_WARN, DOLPHIN_TAG, "Getting size of file: %s", filename.c_str()); - - std::unique_ptr volume(DiscIO::CreateVolumeFromFilename(filename)); - if (volume == nullptr) - return -1; - - u64 size = volume->GetSize(); - __android_log_print(ANDROID_LOG_INFO, DOLPHIN_TAG, "Size: %" PRIu64, size); - return size; -} - -static std::string GetJString(JNIEnv* env, jstring jstr) -{ - std::string result = ""; - if (!jstr) - return result; - - const char* s = env->GetStringUTFChars(jstr, nullptr); - result = s; - env->ReleaseStringUTFChars(jstr, s); - return result; -} - #ifdef __cplusplus extern "C" { #endif @@ -395,28 +172,6 @@ JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_onGamePa JNIEnv* env, jobject obj, jstring jDevice, jint Button, jint Action); JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_onGamePadMoveEvent( JNIEnv* env, jobject obj, jstring jDevice, jint Axis, jfloat Value); -JNIEXPORT jintArray JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetBanner(JNIEnv* env, - jobject obj, - jstring jFile); -JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetTitle(JNIEnv* env, - jobject obj, - jstring jFilename); -JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetDescription( - JNIEnv* env, jobject obj, jstring jFilename); -JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetGameId(JNIEnv* env, - jobject obj, - jstring jFilename); -JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetCountry(JNIEnv* env, - jobject obj, - jstring jFilename); -JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetCompany( - JNIEnv* env, jobject obj, jstring jFilename); -JNIEXPORT jlong JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetFilesize(JNIEnv* env, - jobject obj, - jstring jFilename); -JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetPlatform(JNIEnv* env, - jobject obj, - jstring jFilename); JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetVersionString(JNIEnv* env, jobject obj); JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetGitRevision(JNIEnv* env, @@ -461,8 +216,6 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SetProfiling jboolean enable); JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_WriteProfileResults(JNIEnv* env, jobject obj); -JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_NativeLibrary_CacheClassesAndMethods(JNIEnv* env, jobject obj); JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_Run__Ljava_lang_String_2( JNIEnv* env, jobject obj, jstring jFile); JNIEXPORT void JNICALL @@ -518,93 +271,16 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_onGamePadMov ButtonManager::GamepadAxisEvent(GetJString(env, jDevice), Axis, Value); } -JNIEXPORT jintArray JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetBanner(JNIEnv* env, - jobject obj, - jstring jFile) -{ - std::string file = GetJString(env, jFile); - u32 uBanner[DVD_BANNER_WIDTH * DVD_BANNER_HEIGHT]; - jintArray Banner = env->NewIntArray(DVD_BANNER_WIDTH * DVD_BANNER_HEIGHT); - - if (LoadBanner(file, uBanner)) - { - env->SetIntArrayRegion(Banner, 0, DVD_BANNER_WIDTH * DVD_BANNER_HEIGHT, (jint*)uBanner); - } - return Banner; -} - -JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetTitle(JNIEnv* env, - jobject obj, - jstring jFilename) -{ - std::string filename = GetJString(env, jFilename); - std::string name = GetTitle(filename); - return env->NewStringUTF(name.c_str()); -} - -JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetDescription( - JNIEnv* env, jobject obj, jstring jFilename) -{ - std::string filename = GetJString(env, jFilename); - std::string description = GetDescription(filename); - return env->NewStringUTF(description.c_str()); -} - -JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetGameId(JNIEnv* env, - jobject obj, - jstring jFilename) -{ - std::string filename = GetJString(env, jFilename); - std::string id = GetGameId(filename); - return env->NewStringUTF(id.c_str()); -} - -JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetCompany(JNIEnv* env, - jobject obj, - jstring jFilename) -{ - std::string filename = GetJString(env, jFilename); - std::string company = GetCompany(filename); - return env->NewStringUTF(company.c_str()); -} - -JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetCountry(JNIEnv* env, - jobject obj, - jstring jFilename) -{ - std::string filename = GetJString(env, jFilename); - int country = GetCountry(filename); - return country; -} - -JNIEXPORT jlong JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetFilesize(JNIEnv* env, - jobject obj, - jstring jFilename) -{ - std::string filename = GetJString(env, jFilename); - u64 size = GetFileSize(filename); - return size; -} - -JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetPlatform(JNIEnv* env, - jobject obj, - jstring jFilename) -{ - std::string filename = GetJString(env, jFilename); - int platform = GetPlatform(filename); - return platform; -} - JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetVersionString(JNIEnv* env, jobject obj) { - return env->NewStringUTF(Common::scm_rev_str.c_str()); + return ToJString(env, Common::scm_rev_str.c_str()); } JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetGitRevision(JNIEnv* env, jobject obj) { - return env->NewStringUTF(Common::scm_rev_git_str.c_str()); + return ToJString(env, Common::scm_rev_git_str.c_str()); } JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SaveScreenShot(JNIEnv* env, @@ -646,7 +322,7 @@ JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetUserSe ini.GetOrCreateSection(section)->Get(key, &value, "-1"); - return env->NewStringUTF(value.c_str()); + return ToJString(env, value.c_str()); } JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SetUserSetting( @@ -686,8 +362,9 @@ JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetConfig ini.GetOrCreateSection(section)->Get(key, &value, defaultValue); - return env->NewStringUTF(value.c_str()); + return ToJString(env, value.c_str()); } + JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SetConfig( JNIEnv* env, jobject obj, jstring jFile, jstring jSection, jstring jKey, jstring jValue) { @@ -762,7 +439,7 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SetUserDirec JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetUserDirectory(JNIEnv* env, jobject obj) { - return env->NewStringUTF(File::GetUserPath(D_USER_IDX).c_str()); + return ToJString(env, File::GetUserPath(D_USER_IDX).c_str()); } JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_DefaultCPUCore(JNIEnv* env, @@ -791,25 +468,6 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_WriteProfile JitInterface::WriteProfileResults(filename); } -JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_NativeLibrary_CacheClassesAndMethods(JNIEnv* env, jobject obj) -{ - // This class reference is only valid for the lifetime of this method. - jclass localClass = env->FindClass("org/dolphinemu/dolphinemu/NativeLibrary"); - - // This reference, however, is valid until we delete it. - s_jni_class = reinterpret_cast(env->NewGlobalRef(localClass)); - - // TODO Find a place for this. - // So we don't leak a reference to NativeLibrary.class. - // env->DeleteGlobalRef(s_jni_class); - - // Method signature taken from javap -s - // Source/Android/app/build/intermediates/classes/arm/debug/org/dolphinemu/dolphinemu/NativeLibrary.class - s_jni_method_alert = env->GetStaticMethodID(s_jni_class, "displayAlertMsg", - "(Ljava/lang/String;Ljava/lang/String;Z)Z"); -} - // Surface Handling JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_SurfaceChanged(JNIEnv* env, jobject obj, diff --git a/Source/Core/Core/HW/WiimoteReal/IOAndroid.cpp b/Source/Core/Core/HW/WiimoteReal/IOAndroid.cpp index abc33c88a2..efc8ca4aaf 100644 --- a/Source/Core/Core/HW/WiimoteReal/IOAndroid.cpp +++ b/Source/Core/Core/HW/WiimoteReal/IOAndroid.cpp @@ -14,8 +14,7 @@ #include "Core/HW/WiimoteReal/IOAndroid.h" -// Global java_vm class -extern JavaVM* g_java_vm; +#include "jni/AndroidCommon/IDCache.h" namespace WiimoteReal { @@ -31,10 +30,11 @@ void WiimoteScannerAndroid::FindWiimotes(std::vector& found_wiimotes, NOTICE_LOG(WIIMOTE, "Finding Wiimotes"); JNIEnv* env; - int get_env_status = g_java_vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6); + int get_env_status = + IDCache::GetJavaVM()->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6); if (get_env_status == JNI_EDETACHED) - g_java_vm->AttachCurrentThread(&env, nullptr); + IDCache::GetJavaVM()->AttachCurrentThread(&env, nullptr); jmethodID openadapter_func = env->GetStaticMethodID(s_adapter_class, "OpenAdapter", "()Z"); jmethodID queryadapter_func = env->GetStaticMethodID(s_adapter_class, "QueryAdapter", "()Z"); @@ -47,7 +47,7 @@ void WiimoteScannerAndroid::FindWiimotes(std::vector& found_wiimotes, } if (get_env_status == JNI_EDETACHED) - g_java_vm->DetachCurrentThread(); + IDCache::GetJavaVM()->DetachCurrentThread(); } WiimoteAndroid::WiimoteAndroid(int index) : Wiimote(), m_mayflash_index(index) @@ -62,7 +62,7 @@ WiimoteAndroid::~WiimoteAndroid() // Connect to a Wiimote with a known address. bool WiimoteAndroid::ConnectInternal() { - g_java_vm->AttachCurrentThread(&m_env, nullptr); + IDCache::GetJavaVM()->AttachCurrentThread(&m_env, nullptr); jfieldID payload_field = m_env->GetStaticFieldID(s_adapter_class, "wiimote_payload", "[[B"); jobjectArray payload_object = @@ -81,7 +81,7 @@ bool WiimoteAndroid::ConnectInternal() void WiimoteAndroid::DisconnectInternal() { - g_java_vm->DetachCurrentThread(); + IDCache::GetJavaVM()->DetachCurrentThread(); } bool WiimoteAndroid::IsConnected() const @@ -117,7 +117,7 @@ int WiimoteAndroid::IOWrite(u8 const* buf, size_t len) void InitAdapterClass() { JNIEnv* env; - g_java_vm->AttachCurrentThread(&env, nullptr); + IDCache::GetJavaVM()->AttachCurrentThread(&env, nullptr); jclass adapter_class = env->FindClass("org/dolphinemu/dolphinemu/utils/Java_WiimoteAdapter"); s_adapter_class = reinterpret_cast(env->NewGlobalRef(adapter_class)); diff --git a/Source/Core/InputCommon/GCAdapter_Android.cpp b/Source/Core/InputCommon/GCAdapter_Android.cpp index d8628530a3..d71e665015 100644 --- a/Source/Core/InputCommon/GCAdapter_Android.cpp +++ b/Source/Core/InputCommon/GCAdapter_Android.cpp @@ -20,8 +20,7 @@ #include "InputCommon/GCAdapter.h" #include "InputCommon/GCPadStatus.h" -// Global java_vm class -extern JavaVM* g_java_vm; +#include "jni/AndroidCommon/IDCache.h" namespace GCAdapter { @@ -67,7 +66,7 @@ static void ScanThreadFunc() NOTICE_LOG(SERIALINTERFACE, "GC Adapter scanning thread started"); JNIEnv* env; - g_java_vm->AttachCurrentThread(&env, NULL); + IDCache::GetJavaVM()->AttachCurrentThread(&env, NULL); jmethodID queryadapter_func = env->GetStaticMethodID(s_adapter_class, "QueryAdapter", "()Z"); @@ -78,7 +77,7 @@ static void ScanThreadFunc() Setup(); Common::SleepCurrentThread(1000); } - g_java_vm->DetachCurrentThread(); + IDCache::GetJavaVM()->DetachCurrentThread(); NOTICE_LOG(SERIALINTERFACE, "GC Adapter scanning thread stopped"); } @@ -89,7 +88,7 @@ static void Write() NOTICE_LOG(SERIALINTERFACE, "GC Adapter write thread started"); JNIEnv* env; - g_java_vm->AttachCurrentThread(&env, NULL); + IDCache::GetJavaVM()->AttachCurrentThread(&env, NULL); jmethodID output_func = env->GetStaticMethodID(s_adapter_class, "Output", "([B)I"); while (s_write_adapter_thread_running.IsSet()) @@ -119,7 +118,7 @@ static void Write() Common::YieldCPU(); } - g_java_vm->DetachCurrentThread(); + IDCache::GetJavaVM()->DetachCurrentThread(); NOTICE_LOG(SERIALINTERFACE, "GC Adapter write thread stopped"); } @@ -131,7 +130,7 @@ static void Read() bool first_read = true; JNIEnv* env; - g_java_vm->AttachCurrentThread(&env, NULL); + IDCache::GetJavaVM()->AttachCurrentThread(&env, NULL); jfieldID payload_field = env->GetStaticFieldID(s_adapter_class, "controller_payload", "[B"); jobject payload_object = env->GetStaticObjectField(s_adapter_class, payload_field); @@ -185,7 +184,7 @@ static void Read() s_fd = 0; s_detected = false; - g_java_vm->DetachCurrentThread(); + IDCache::GetJavaVM()->DetachCurrentThread(); NOTICE_LOG(SERIALINTERFACE, "GC Adapter read thread stopped"); } @@ -204,7 +203,7 @@ void Init() } JNIEnv* env; - g_java_vm->AttachCurrentThread(&env, NULL); + IDCache::GetJavaVM()->AttachCurrentThread(&env, NULL); jclass adapter_class = env->FindClass("org/dolphinemu/dolphinemu/utils/Java_GCAdapter"); s_adapter_class = reinterpret_cast(env->NewGlobalRef(adapter_class)); diff --git a/Source/Core/UICommon/GameFile.cpp b/Source/Core/UICommon/GameFile.cpp index 64d4740a0e..a666efdaa5 100644 --- a/Source/Core/UICommon/GameFile.cpp +++ b/Source/Core/UICommon/GameFile.cpp @@ -78,8 +78,15 @@ const std::string& GameFile::Lookup(DiscIO::Language language, const std::string& GameFile::LookupUsingConfigLanguage(const std::map& strings) const { +#ifdef ANDROID + // TODO: Make the Android app load the config at app start instead of emulation start + // so that we can access the user's preference here + const DiscIO::Language language = DiscIO::Language::English; +#else const bool wii = DiscIO::IsWii(m_platform); - return Lookup(SConfig::GetInstance().GetCurrentLanguage(wii), strings); + const DiscIO::Language language = SConfig::GetInstance().GetCurrentLanguage(wii); +#endif + return Lookup(language, strings); } GameFile::GameFile(const std::string& path) diff --git a/Source/Core/UICommon/GameFileCache.cpp b/Source/Core/UICommon/GameFileCache.cpp index 7a71ca75f1..074dc7fe62 100644 --- a/Source/Core/UICommon/GameFileCache.cpp +++ b/Source/Core/UICommon/GameFileCache.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include "Common/ChunkFile.h" @@ -38,12 +39,25 @@ std::vector FindAllGamePaths(const std::vector& direct return Common::DoFileSearch(directories_to_scan, search_extensions, recursive_scan); } +GameFileCache::GameFileCache() : m_path(File::GetUserPath(D_CACHE_IDX) + "gamelist.cache") +{ +} + +GameFileCache::GameFileCache(std::string path) : m_path(std::move(path)) +{ +} + void GameFileCache::ForEach(std::function&)> f) const { for (const std::shared_ptr& item : m_cached_files) f(item); } +size_t GameFileCache::GetSize() const +{ + return m_cached_files.size(); +} + void GameFileCache::Clear() { m_cached_files.clear(); @@ -179,9 +193,8 @@ bool GameFileCache::Save() bool GameFileCache::SyncCacheFile(bool save) { - std::string filename(File::GetUserPath(D_CACHE_IDX) + "gamelist.cache"); const char* open_mode = save ? "wb" : "rb"; - File::IOFile f(filename, open_mode); + File::IOFile f(m_path, open_mode); if (!f) return false; bool success = false; @@ -217,7 +230,7 @@ bool GameFileCache::SyncCacheFile(bool save) { // If some file operation failed, try to delete the probably-corrupted cache f.Close(); - File::Delete(filename); + File::Delete(m_path); } return success; } diff --git a/Source/Core/UICommon/GameFileCache.h b/Source/Core/UICommon/GameFileCache.h index 6e36838dd0..197cceb547 100644 --- a/Source/Core/UICommon/GameFileCache.h +++ b/Source/Core/UICommon/GameFileCache.h @@ -6,9 +6,7 @@ #include #include -#include #include -#include #include #include @@ -26,8 +24,12 @@ std::vector FindAllGamePaths(const std::vector& direct class GameFileCache { public: + GameFileCache(); // Uses the default path + explicit GameFileCache(std::string path); + void ForEach(std::function&)> f) const; + size_t GetSize() const; void Clear(); // Returns nullptr if the file is invalid. @@ -49,6 +51,7 @@ private: bool SyncCacheFile(bool save); void DoState(PointerWrap* p, u64 size = 0); + std::string m_path; std::vector> m_cached_files; };