Android: Implement an SQLite database-based game library.

This commit is contained in:
Eder Bastos 2015-05-17 18:12:11 -04:00
parent 4f6a5e0293
commit 3e5e352fee
13 changed files with 533 additions and 168 deletions

View File

@ -48,20 +48,20 @@
</activity> </activity>
<activity <activity
android:name="org.dolphinemu.dolphinemu.about.AboutActivity" android:name=".about.AboutActivity"
android:theme="@android:style/Theme.Holo.Light" /> android:theme="@android:style/Theme.Holo.Light" />
<activity <activity
android:name="org.dolphinemu.dolphinemu.emulation.EmulationActivity" android:name=".emulation.EmulationActivity"
android:screenOrientation="landscape" /> android:screenOrientation="landscape" />
<activity <activity
android:name="org.dolphinemu.dolphinemu.settings.input.overlayconfig.OverlayConfigActivity" android:name=".settings.input.overlayconfig.OverlayConfigActivity"
android:screenOrientation="landscape" android:screenOrientation="landscape"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"/> android:theme="@android:style/Theme.NoTitleBar.Fullscreen"/>
<activity <activity
android:name="org.dolphinemu.dolphinemu.settings.PrefsActivity" android:name=".settings.PrefsActivity"
android:label="@string/settings" android:label="@string/settings"
android:theme="@android:style/Theme.Holo.Light" /> android:theme="@android:style/Theme.Holo.Light" />
@ -69,6 +69,13 @@
<service android:name=".services.SettingsSaveService"/> <service android:name=".services.SettingsSaveService"/>
<provider
android:name=".model.GameProvider"
android:authorities="${applicationId}.provider"
android:enabled="true"
android:exported="false">
</provider>
</application> </application>
</manifest> </manifest>

View File

@ -1,7 +1,10 @@
package org.dolphinemu.dolphinemu.activities; package org.dolphinemu.dolphinemu.activities;
import android.app.Activity; import android.app.Activity;
import android.content.AsyncQueryHandler;
import android.content.ContentValues;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
@ -14,6 +17,8 @@ import android.widget.Toolbar;
import org.dolphinemu.dolphinemu.BuildConfig; import org.dolphinemu.dolphinemu.BuildConfig;
import org.dolphinemu.dolphinemu.R; import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.adapters.FileAdapter; import org.dolphinemu.dolphinemu.adapters.FileAdapter;
import org.dolphinemu.dolphinemu.model.GameDatabase;
import org.dolphinemu.dolphinemu.model.GameProvider;
/** /**
* An Activity that shows a list of files and folders, allowing the user to tell the app which folder(s) * An Activity that shows a list of files and folders, allowing the user to tell the app which folder(s)
@ -91,17 +96,36 @@ public class AddDirectoryActivity extends Activity implements FileAdapter.FileCl
} }
/** /**
* Tell the GameGridActivity that launched this Activity that the user picked a folder. * Add a directory to the library, and if successful, end the activity.
*
* @param path The target directory's path.
*/ */
@Override @Override
public void finishSuccessfully() public void addDirectory()
{ {
Intent resultData = new Intent(); // Set up a callback for when the addition is complete
// TODO This has a nasty warning on it; find a cleaner way to do this Insert asynchronously
AsyncQueryHandler handler = new AsyncQueryHandler(getContentResolver())
{
@Override
protected void onInsertComplete(int token, Object cookie, Uri uri)
{
Intent resultData = new Intent();
resultData.putExtra(KEY_CURRENT_PATH, mAdapter.getPath()); resultData.putExtra(KEY_CURRENT_PATH, mAdapter.getPath());
setResult(RESULT_OK, resultData); setResult(RESULT_OK, resultData);
finish(); finish();
}
};
ContentValues file = new ContentValues();
file.put(GameDatabase.KEY_FOLDER_PATH, mAdapter.getPath());
handler.startInsert(0, // We don't need to identify this call to the handler
null, // We don't need to pass additional data to the handler
GameProvider.URI_FOLDER, // Tell the GameProvider we are adding a folder
file); // Tell the GameProvider what folder we are adding
} }
@Override @Override

View File

@ -1,13 +1,15 @@
package org.dolphinemu.dolphinemu.activities; package org.dolphinemu.dolphinemu.activities;
import android.app.Activity; import android.app.Activity;
import android.app.LoaderManager;
import android.content.CursorLoader;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.Loader;
import android.database.Cursor;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment;
import android.preference.PreferenceManager;
import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
@ -15,27 +17,23 @@ import android.view.View;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.Toolbar; import android.widget.Toolbar;
import org.dolphinemu.dolphinemu.NativeLibrary;
import org.dolphinemu.dolphinemu.R; import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.adapters.GameAdapter; import org.dolphinemu.dolphinemu.adapters.GameAdapter;
import org.dolphinemu.dolphinemu.model.Game; import org.dolphinemu.dolphinemu.model.GameDatabase;
import org.dolphinemu.dolphinemu.model.GcGame; import org.dolphinemu.dolphinemu.model.GameProvider;
import org.dolphinemu.dolphinemu.services.AssetCopyService; import org.dolphinemu.dolphinemu.services.AssetCopyService;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/** /**
* The main Activity of the Lollipop style UI. Shows a grid of games on tablets & landscape phones, * The main Activity of the Lollipop style UI. Shows a grid of games on tablets & landscape phones,
* shows a list of games on portrait phones. * shows a list of games on portrait phones.
*/ */
public final class GameGridActivity extends Activity public final class GameGridActivity extends Activity implements LoaderManager.LoaderCallbacks<Cursor>
{ {
private static final int REQUEST_ADD_DIRECTORY = 1; private static final int REQUEST_ADD_DIRECTORY = 1;
private static final int LOADER_ID_GAMES = 1;
// TODO When each platform has its own tab, there should be a LOADER_ID for each platform.
private GameAdapter mAdapter; private GameAdapter mAdapter;
@Override @Override
@ -62,7 +60,8 @@ public final class GameGridActivity extends Activity
recyclerView.addItemDecoration(new GameAdapter.SpacesItemDecoration(8)); recyclerView.addItemDecoration(new GameAdapter.SpacesItemDecoration(8));
// Create an adapter that will relate the dataset to the views on-screen. // Create an adapter that will relate the dataset to the views on-screen.
mAdapter = new GameAdapter(getGameList()); getLoaderManager().initLoader(LOADER_ID_GAMES, null, this);
mAdapter = new GameAdapter();
recyclerView.setAdapter(mAdapter); recyclerView.setAdapter(mAdapter);
buttonAddDirectory.setOnClickListener(new View.OnClickListener() buttonAddDirectory.setOnClickListener(new View.OnClickListener()
@ -103,20 +102,7 @@ public final class GameGridActivity extends Activity
// other activities might use this callback in the future (don't forget to change Javadoc!) // other activities might use this callback in the future (don't forget to change Javadoc!)
if (requestCode == REQUEST_ADD_DIRECTORY) if (requestCode == REQUEST_ADD_DIRECTORY)
{ {
// Get the path the user selected in AddDirectoryActivity. getLoaderManager().restartLoader(LOADER_ID_GAMES, null, this);
String path = result.getStringExtra(AddDirectoryActivity.KEY_CURRENT_PATH);
// Store this path as a preference.
// TODO Use SQLite instead.
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
SharedPreferences.Editor editor = prefs.edit();
editor.putString(AddDirectoryActivity.KEY_CURRENT_PATH, path);
// Using commit, not apply, in order to block so the next method has the correct data to load.
editor.commit();
mAdapter.setGameList(getGameList());
} }
} }
} }
@ -150,56 +136,75 @@ public final class GameGridActivity extends Activity
return false; return false;
} }
// TODO Replace all of this with a SQLite database
private ArrayList<Game> getGameList() /**
* Callback that's invoked when the system has initialized the Loader and
* is ready to start the query. This usually happens when initLoader() is
* called. Here, we use it to make a DB query for games.
*
* @param id The ID value passed to the initLoader() call that triggered this.
* @param args The args bundle supplied by the caller.
* @return A new Loader instance that is ready to start loading.
*/
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args)
{ {
ArrayList<Game> gameList = new ArrayList<Game>(); Log.d("DolphinEmu", "Creating loader with id: " + id);
final String DefaultDir = Environment.getExternalStorageDirectory() + File.separator + "dolphin-emu"; // Take action based on the ID of the Loader that's being created.
switch (id)
NativeLibrary.SetUserDirectory(DefaultDir);
// Extensions to filter by.
Set<String> exts = new HashSet<String>(Arrays.asList(".dff", ".dol", ".elf", ".gcm", ".gcz", ".iso", ".wad", ".wbfs"));
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
String path = prefs.getString(AddDirectoryActivity.KEY_CURRENT_PATH, "/");
File currentDir = new File(path);
File[] dirs = currentDir.listFiles();
try
{ {
for (File entry : dirs) case LOADER_ID_GAMES:
{ // TODO Play some sort of load-starting animation; maybe fade the list out.
if (!entry.isHidden() && !entry.isDirectory())
{
String entryName = entry.getName();
// Check that the file has an appropriate extension before trying to read out of it. return new CursorLoader(
if (exts.contains(entryName.toLowerCase().substring(entryName.lastIndexOf('.')))) this, // Parent activity context
{ GameProvider.URI_GAME, // URI of table to query
String absolutePath = entry.getAbsolutePath(); null, // Return all columns
Game game = new Game(NativeLibrary.GetPlatform(absolutePath), null, // No selection clause
NativeLibrary.GetTitle(absolutePath), null, // No selection arguments
NativeLibrary.GetDescription(absolutePath).replace("\n", " "), GameDatabase.KEY_GAME_TITLE + " asc" // Sort by game name, ascending order
NativeLibrary.GetCountry(absolutePath), );
absolutePath,
NativeLibrary.GetGameId(absolutePath),
NativeLibrary.GetCompany(absolutePath));
gameList.add(game); default:
} Log.e("DolphinEmu", "Bad ID passed in.");
return null;
}
}
} }
catch (Exception ignored) }
{
/**
* Callback that's invoked when the Loader returned in onCreateLoader is finished
* with its task. In this case, the game DB query is finished, so we should put the results
* on screen.
*
* @param loader The loader that finished.
* @param data The data the Loader loaded.
*/
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data)
{
int id = loader.getId();
Log.d("DolphinEmu", "Loader finished with id: " + id);
// TODO When each platform has its own tab, this should just call into those tabs instead.
switch (id)
{
case LOADER_ID_GAMES:
mAdapter.swapCursor(data);
// TODO Play some sort of load-finished animation; maybe fade the list in.
break;
default:
Log.e("DolphinEmu", "Bad ID passed in.");
} }
return gameList; }
@Override
public void onLoaderReset(Loader<Cursor> loader)
{
Log.d("DolphinEmu", "Loader resetting.");
// TODO ¯\_()_/¯
} }
} }

View File

@ -14,7 +14,7 @@ import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
public class FileAdapter extends RecyclerView.Adapter<FileViewHolder> implements View.OnClickListener public final class FileAdapter extends RecyclerView.Adapter<FileViewHolder> implements View.OnClickListener
{ {
private ArrayList<FileListItem> mFileList; private ArrayList<FileListItem> mFileList;
@ -146,7 +146,7 @@ public class FileAdapter extends RecyclerView.Adapter<FileViewHolder> implements
else else
{ {
// Pass the activity the path of the parent directory of the clicked file. // Pass the activity the path of the parent directory of the clicked file.
mListener.finishSuccessfully(); mListener.addDirectory();
} }
} }
@ -154,7 +154,7 @@ public class FileAdapter extends RecyclerView.Adapter<FileViewHolder> implements
* For a given directory, return a list of Files it contains. * For a given directory, return a list of Files it contains.
* *
* @param directory A File representing the directory that should have its contents displayed. * @param directory A File representing the directory that should have its contents displayed.
* @return * @return The list of files contained in the directory.
*/ */
private ArrayList<FileListItem> generateFileList(File directory) private ArrayList<FileListItem> generateFileList(File directory)
{ {
@ -205,7 +205,7 @@ public class FileAdapter extends RecyclerView.Adapter<FileViewHolder> implements
*/ */
public interface FileClickListener public interface FileClickListener
{ {
void finishSuccessfully(); void addDirectory();
void updateSubtitle(String path); void updateSubtitle(String path);
} }

View File

@ -2,8 +2,11 @@ package org.dolphinemu.dolphinemu.adapters;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.graphics.Rect; import android.graphics.Rect;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -13,25 +16,31 @@ import com.squareup.picasso.Picasso;
import org.dolphinemu.dolphinemu.R; import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.dialogs.GameDetailsDialog; import org.dolphinemu.dolphinemu.dialogs.GameDetailsDialog;
import org.dolphinemu.dolphinemu.emulation.EmulationActivity; import org.dolphinemu.dolphinemu.emulation.EmulationActivity;
import org.dolphinemu.dolphinemu.model.Game; import org.dolphinemu.dolphinemu.model.GameDatabase;
import org.dolphinemu.dolphinemu.viewholders.GameViewHolder; import org.dolphinemu.dolphinemu.viewholders.GameViewHolder;
import java.util.ArrayList; /**
* This adapter, unlike {@link FileAdapter} which is backed by an ArrayList, gets its
public class GameAdapter extends RecyclerView.Adapter<GameViewHolder> implements * information from a database Cursor. This fact, paired with the usage of ContentProviders
* and Loaders, allows for efficient display of a limited view into a (possibly) large dataset.
*/
public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> implements
View.OnClickListener, View.OnClickListener,
View.OnLongClickListener View.OnLongClickListener
{ {
private ArrayList<Game> mGameList; private Cursor mCursor;
private GameDataSetObserver mObserver;
private boolean mDatasetValid;
/** /**
* Mostly just initializes the dataset to be displayed. * 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.
* @param gameList
*/ */
public GameAdapter(ArrayList<Game> gameList) public GameAdapter()
{ {
mGameList = gameList; mDatasetValid = false;
mObserver = new GameDataSetObserver();
} }
/** /**
@ -52,8 +61,7 @@ public class GameAdapter extends RecyclerView.Adapter<GameViewHolder> implements
gameCard.setOnLongClickListener(this); gameCard.setOnLongClickListener(this);
// Use that view to create a ViewHolder. // Use that view to create a ViewHolder.
GameViewHolder holder = new GameViewHolder(gameCard); return new GameViewHolder(gameCard);
return holder;
} }
/** /**
@ -67,26 +75,41 @@ public class GameAdapter extends RecyclerView.Adapter<GameViewHolder> implements
@Override @Override
public void onBindViewHolder(GameViewHolder holder, int position) public void onBindViewHolder(GameViewHolder holder, int position)
{ {
// Get a reference to the item from the dataset; we'll use this to fill in the view contents. if (mDatasetValid)
final Game game = mGameList.get(position);
// Fill in the view contents.
Picasso.with(holder.imageScreenshot.getContext())
.load(game.getScreenPath())
.fit()
.centerCrop()
.error(R.drawable.no_banner)
.into(holder.imageScreenshot);
holder.textGameTitle.setText(game.getTitle());
if (game.getCompany() != null)
{ {
holder.textCompany.setText(game.getCompany()); if (mCursor.moveToPosition(position))
{
// Fill in the view contents.
Picasso.with(holder.imageScreenshot.getContext())
.load(mCursor.getString(GameDatabase.GAME_COLUMN_SCREENSHOT_PATH))
.fit()
.centerCrop()
.error(R.drawable.no_banner)
.into(holder.imageScreenshot);
holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE));
holder.textCompany.setText(mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY));
// 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.e("DolphinEmu", "Can't bind view; Cursor is not valid.");
}
}
else
{
Log.e("DolphinEmu", "Can't bind view; dataset is not valid.");
} }
holder.path = game.getPath();
holder.screenshotPath = game.getScreenPath();
holder.game = game;
} }
/** /**
@ -97,7 +120,85 @@ public class GameAdapter extends RecyclerView.Adapter<GameViewHolder> implements
@Override @Override
public int getItemCount() public int getItemCount()
{ {
return mGameList.size(); if (mDatasetValid && mCursor != null)
{
return mCursor.getCount();
}
Log.e("DolphinEmu", "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.e("DolphinEmu", "Dataset is not valid.");
return 0;
}
/**
* 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);
}
/**
* When a load is finished, call this to replace the existing data with the newly-loaded
* data.
*
* @param cursor The newly-loaded Cursor.
*/
public void swapCursor(Cursor cursor)
{
// 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;
}
notifyDataSetChanged();
} }
/** /**
@ -134,7 +235,12 @@ public class GameAdapter extends RecyclerView.Adapter<GameViewHolder> implements
// String gameId = (String) holder.gameId; // String gameId = (String) holder.gameId;
Activity activity = (Activity) view.getContext(); Activity activity = (Activity) view.getContext();
GameDetailsDialog.newInstance(holder.game).show(activity.getFragmentManager(), "game_details"); GameDetailsDialog.newInstance(holder.title,
holder.description,
holder.country,
holder.company,
holder.path,
holder.screenshotPath).show(activity.getFragmentManager(), "game_details");
return true; return true;
} }
@ -158,9 +264,24 @@ public class GameAdapter extends RecyclerView.Adapter<GameViewHolder> implements
} }
} }
public void setGameList(ArrayList<Game> gameList) private final class GameDataSetObserver extends DataSetObserver
{ {
mGameList = gameList; @Override
notifyDataSetChanged(); public void onChanged()
{
super.onChanged();
mDatasetValid = true;
notifyDataSetChanged();
}
@Override
public void onInvalidated()
{
super.onInvalidated();
mDatasetValid = false;
notifyDataSetChanged();
}
} }
} }

View File

@ -17,11 +17,10 @@ import com.squareup.picasso.Picasso;
import org.dolphinemu.dolphinemu.BuildConfig; import org.dolphinemu.dolphinemu.BuildConfig;
import org.dolphinemu.dolphinemu.R; import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.emulation.EmulationActivity; import org.dolphinemu.dolphinemu.emulation.EmulationActivity;
import org.dolphinemu.dolphinemu.model.Game;
import de.hdodenhof.circleimageview.CircleImageView; import de.hdodenhof.circleimageview.CircleImageView;
public class GameDetailsDialog extends DialogFragment public final class GameDetailsDialog extends DialogFragment
{ {
public static final String ARGUMENT_GAME_TITLE = BuildConfig.APPLICATION_ID + ".game_title"; public static final String ARGUMENT_GAME_TITLE = BuildConfig.APPLICATION_ID + ".game_title";
public static final String ARGUMENT_GAME_DESCRIPTION = BuildConfig.APPLICATION_ID + ".game_description"; public static final String ARGUMENT_GAME_DESCRIPTION = BuildConfig.APPLICATION_ID + ".game_description";
@ -30,18 +29,18 @@ public class GameDetailsDialog extends DialogFragment
public static final String ARGUMENT_GAME_PATH = BuildConfig.APPLICATION_ID + ".game_path"; public static final String ARGUMENT_GAME_PATH = BuildConfig.APPLICATION_ID + ".game_path";
public static final String ARGUMENT_GAME_SCREENSHOT_PATH = BuildConfig.APPLICATION_ID + ".game_screenshot_path"; public static final String ARGUMENT_GAME_SCREENSHOT_PATH = BuildConfig.APPLICATION_ID + ".game_screenshot_path";
// TODO Add all of this to the Loader in GameActivity.java
public static GameDetailsDialog newInstance(Game game) public static GameDetailsDialog newInstance(String title, String description, int country, String company, String path, String screenshotPath)
{ {
GameDetailsDialog fragment = new GameDetailsDialog(); GameDetailsDialog fragment = new GameDetailsDialog();
Bundle arguments = new Bundle(); Bundle arguments = new Bundle();
arguments.putString(ARGUMENT_GAME_TITLE, game.getTitle()); arguments.putString(ARGUMENT_GAME_TITLE, title);
arguments.putString(ARGUMENT_GAME_DESCRIPTION, game.getDescription()); arguments.putString(ARGUMENT_GAME_DESCRIPTION, description);
arguments.putInt(ARGUMENT_GAME_COUNTRY, game.getCountry()); arguments.putInt(ARGUMENT_GAME_COUNTRY, country);
arguments.putString(ARGUMENT_GAME_DATE, game.getCompany()); arguments.putString(ARGUMENT_GAME_DATE, company);
arguments.putString(ARGUMENT_GAME_PATH, game.getPath()); arguments.putString(ARGUMENT_GAME_PATH, path);
arguments.putString(ARGUMENT_GAME_SCREENSHOT_PATH, game.getScreenPath()); arguments.putString(ARGUMENT_GAME_SCREENSHOT_PATH, screenshotPath);
fragment.setArguments(arguments); fragment.setArguments(arguments);
return fragment; return fragment;

View File

@ -10,11 +10,11 @@ import java.util.Set;
public class FileListItem implements Comparable<FileListItem> public class FileListItem implements Comparable<FileListItem>
{ {
public static final int TYPE_GC = 0; public static final int TYPE_FOLDER = 0;
public static final int TYPE_WII = 1; public static final int TYPE_GC = 1;
public static final int TYPE_WII_WARE = 2; public static final int TYPE_WII = 2;
public static final int TYPE_WII_WARE = 3;
public static final int TYPE_OTHER = 4; public static final int TYPE_OTHER = 4;
public static final int TYPE_FOLDER = 5;
private int mType; private int mType;
private String mFilename; private String mFilename;
@ -47,7 +47,8 @@ public class FileListItem implements Comparable<FileListItem>
// Check that the file has an extension we care about before trying to read out of it. // Check that the file has an extension we care about before trying to read out of it.
if (allowedExtensions.contains(fileExtension)) if (allowedExtensions.contains(fileExtension))
{ {
mType = NativeLibrary.GetPlatform(mPath); // Add 1 because 0 = TYPE_FOLDER
mType = NativeLibrary.GetPlatform(mPath) + 1;
} }
else else
{ {

View File

@ -1,10 +1,11 @@
package org.dolphinemu.dolphinemu.model; package org.dolphinemu.dolphinemu.model;
import android.content.ContentValues; import android.content.ContentValues;
import android.database.Cursor;
import java.io.File; import java.io.File;
public class Game public final class Game
{ {
public static final int PLATFORM_GC = 0; public static final int PLATFORM_GC = 0;
public static final int PLATFORM_WII = 1; public static final int PLATFORM_WII = 1;
@ -112,6 +113,22 @@ public class Game
{ {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
// TODO Come up with a way of finding the most recent screenshot that doesn't involve counting files
String screenshotFolderPath = PATH_SCREENSHOT_FOLDER + gameId + "/";
// Count how many screenshots are available, so we can use the most recent one.
File screenshotFolder = new File(screenshotFolderPath.substring(screenshotFolderPath.indexOf('s') - 1));
int screenCount = 0;
if (screenshotFolder.isDirectory())
{
screenCount = screenshotFolder.list().length;
}
String screenPath = screenshotFolderPath
+ gameId + "-"
+ screenCount + ".png";
values.put(GameDatabase.KEY_GAME_PLATFORM, platform); values.put(GameDatabase.KEY_GAME_PLATFORM, platform);
values.put(GameDatabase.KEY_GAME_TITLE, title); values.put(GameDatabase.KEY_GAME_TITLE, title);
values.put(GameDatabase.KEY_GAME_DESCRIPTION, description); values.put(GameDatabase.KEY_GAME_DESCRIPTION, description);
@ -119,7 +136,19 @@ public class Game
values.put(GameDatabase.KEY_GAME_PATH, path); values.put(GameDatabase.KEY_GAME_PATH, path);
values.put(GameDatabase.KEY_GAME_ID, gameId); values.put(GameDatabase.KEY_GAME_ID, gameId);
values.put(GameDatabase.KEY_GAME_COMPANY, company); values.put(GameDatabase.KEY_GAME_COMPANY, company);
values.put(GameDatabase.KEY_GAME_SCREENSHOT_PATH, screenPath);
return values; return values;
} }
public static Game fromCursor(Cursor cursor)
{
return new Game(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));
}
} }

View File

@ -14,13 +14,16 @@ import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
public class GameDatabase extends SQLiteOpenHelper /**
* 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 FOLDER_COLUMN_ID = 0; public static final int COLUMN_DB_ID = 0;
public static final int FOLDER_COLUMN_PATH = 1;
public static final int GAME_COLUMN_ID = 0;
public static final int GAME_COLUMN_PATH = 1; public static final int GAME_COLUMN_PATH = 1;
public static final int GAME_COLUMN_PLATFORM = 2; public static final int GAME_COLUMN_PLATFORM = 2;
public static final int GAME_COLUMN_TITLE = 3; public static final int GAME_COLUMN_TITLE = 3;
@ -30,9 +33,9 @@ public class GameDatabase extends SQLiteOpenHelper
public static final int GAME_COLUMN_COMPANY = 7; public static final int GAME_COLUMN_COMPANY = 7;
public static final int GAME_COLUMN_SCREENSHOT_PATH = 8; public static final int GAME_COLUMN_SCREENSHOT_PATH = 8;
public static final String KEY_DB_ID = "_id"; public static final int FOLDER_COLUMN_PATH = 1;
public static final String KEY_FOLDER_PATH = "path"; public static final String KEY_DB_ID = "_id";
public static final String KEY_GAME_PATH = "path"; public static final String KEY_GAME_PATH = "path";
public static final String KEY_GAME_PLATFORM = "platform"; public static final String KEY_GAME_PLATFORM = "platform";
@ -43,30 +46,35 @@ public class GameDatabase extends SQLiteOpenHelper
public static final String KEY_GAME_COMPANY = "company"; public static final String KEY_GAME_COMPANY = "company";
public static final String KEY_GAME_SCREENSHOT_PATH = "screenshot_path"; public static final String KEY_GAME_SCREENSHOT_PATH = "screenshot_path";
private static final int DB_VERSION = 1; public static final String KEY_FOLDER_PATH = "path";
private static final String TABLE_NAME_FOLDERS = "folders"; public static final String TABLE_NAME_FOLDERS = "folders";
private static final String TABLE_NAME_GAMES = "games"; public static final String TABLE_NAME_GAMES = "games";
private static final String TYPE_INTEGER = " INTEGER, "; private static final String TYPE_PRIMARY = " INTEGER PRIMARY KEY";
private static final String TYPE_STRING = " TEXT, "; private static final String TYPE_INTEGER = " INTEGER";
private static final String TYPE_STRING = " TEXT";
private static final String CONSTRAINT_UNIQUE = " UNIQUE";
private static final String SEPARATOR = ", ";
private static final String SQL_CREATE_GAMES = "CREATE TABLE " + TABLE_NAME_GAMES + "(" private static final String SQL_CREATE_GAMES = "CREATE TABLE " + TABLE_NAME_GAMES + "("
+ KEY_DB_ID + " INTEGER PRIMARY KEY, " + KEY_DB_ID + TYPE_PRIMARY + SEPARATOR
+ KEY_GAME_PATH + TYPE_STRING + KEY_GAME_PATH + TYPE_STRING + SEPARATOR
+ KEY_GAME_PLATFORM + TYPE_STRING + KEY_GAME_PLATFORM + TYPE_STRING + SEPARATOR
+ KEY_GAME_TITLE + TYPE_STRING + KEY_GAME_TITLE + TYPE_STRING + SEPARATOR
+ KEY_GAME_DESCRIPTION + TYPE_STRING + KEY_GAME_DESCRIPTION + TYPE_STRING + SEPARATOR
+ KEY_GAME_COUNTRY + TYPE_INTEGER + KEY_GAME_COUNTRY + TYPE_INTEGER + SEPARATOR
+ KEY_GAME_ID + TYPE_STRING + KEY_GAME_ID + TYPE_STRING + SEPARATOR
+ KEY_GAME_COMPANY + TYPE_STRING + KEY_GAME_COMPANY + TYPE_STRING + SEPARATOR
+ KEY_GAME_SCREENSHOT_PATH + TYPE_STRING + ")"; + KEY_GAME_SCREENSHOT_PATH + TYPE_STRING + ")";
private static final String SQL_CREATE_FOLDERS = "CREATE TABLE " + TABLE_NAME_FOLDERS + "(" private static final String SQL_CREATE_FOLDERS = "CREATE TABLE " + TABLE_NAME_FOLDERS + "("
+ KEY_DB_ID + " INTEGER PRIMARY KEY, " + KEY_DB_ID + TYPE_PRIMARY + SEPARATOR
+ KEY_FOLDER_PATH + TYPE_STRING + ")"; + KEY_FOLDER_PATH + TYPE_STRING + CONSTRAINT_UNIQUE + ")";
private static final String SQL_DELETE_GAMES = "DROP TABLE IF EXISTS" + TABLE_NAME_GAMES; private static final String SQL_DELETE_GAMES = "DROP TABLE IF EXISTS " + TABLE_NAME_GAMES;
public GameDatabase(Context context) public GameDatabase(Context context)
{ {
@ -89,18 +97,21 @@ public class GameDatabase extends SQLiteOpenHelper
@Override @Override
public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion) public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion)
{ {
Log.i("DolphinEmu", "Upgrading database."); Log.i("DolphinEmu", "Upgrading database from schema version " + oldVersion + " to " + newVersion);
Log.v("DolphinEmu", "Executing SQL: " + SQL_DELETE_GAMES); Log.v("DolphinEmu", "Executing SQL: " + SQL_DELETE_GAMES);
database.execSQL(SQL_DELETE_GAMES); database.execSQL(SQL_DELETE_GAMES);
Log.v("DolphinEmu", "Executing SQL: " + SQL_CREATE_GAMES); Log.v("DolphinEmu", "Executing SQL: " + SQL_CREATE_GAMES);
database.execSQL(SQL_CREATE_GAMES); database.execSQL(SQL_CREATE_GAMES);
Log.v("DolphinEmu", "Re-scanning library with new schema.");
scanLibrary(database);
} }
public void scanLibrary() public void scanLibrary(SQLiteDatabase database)
{ {
SQLiteDatabase database = getWritableDatabase(); // TODO Before scanning known folders, go through the game table and remove any entries for which the file itself is missing.
// Get a cursor listing all the folders the user has added to the library. // Get a cursor listing all the folders the user has added to the library.
Cursor cursor = database.query(TABLE_NAME_FOLDERS, Cursor cursor = database.query(TABLE_NAME_FOLDERS,
@ -119,9 +130,12 @@ public class GameDatabase extends SQLiteOpenHelper
// Iterate through all results of the DB query (i.e. all folders in the library.) // Iterate through all results of the DB query (i.e. all folders in the library.)
while (cursor.moveToNext()) while (cursor.moveToNext())
{ {
String folderPath = cursor.getString(FOLDER_COLUMN_PATH); String folderPath = cursor.getString(FOLDER_COLUMN_PATH);
File folder = new File(folderPath); File folder = new File(folderPath);
Log.i("DolphinEmu", "Reading files from library folder: " + folderPath);
// Iterate through every file in the folder. // Iterate through every file in the folder.
File[] children = folder.listFiles(); File[] children = folder.listFiles();
for (File file : children) for (File file : children)
@ -146,7 +160,24 @@ public class GameDatabase extends SQLiteOpenHelper
NativeLibrary.GetGameId(filePath), NativeLibrary.GetGameId(filePath),
NativeLibrary.GetCompany(filePath)); NativeLibrary.GetCompany(filePath));
database.insert(TABLE_NAME_GAMES, null, game); // 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.v("DolphinEmu", "Adding game: " + game.getAsString(KEY_GAME_TITLE));
database.insert(TABLE_NAME_GAMES, null, game);
}
else
{
Log.v("DolphinEmu", "Updated game: " + game.getAsString(KEY_GAME_TITLE));
}
} }
} }
} }

View File

@ -0,0 +1,138 @@
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.util.Log;
import org.dolphinemu.dolphinemu.BuildConfig;
/**
* 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 AUTHORITY = "content://" + BuildConfig.APPLICATION_ID + ".provider";
public static final Uri URI_FOLDER = Uri.parse(AUTHORITY + "/" + GameDatabase.TABLE_NAME_FOLDERS + "/");
public static final Uri URI_GAME = Uri.parse(AUTHORITY + "/" + GameDatabase.TABLE_NAME_GAMES + "/");
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.i("DolphinEmu", "Creating Content Provider...");
mDbHelper = new GameDatabase(getContext());
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
{
Log.i("DolphinEmu", "Querying URI: " + uri);
SQLiteDatabase db = mDbHelper.getReadableDatabase();
String table = uri.getLastPathSegment();
if (table == null)
{
Log.e("DolphinEmu", "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(Uri uri)
{
Log.v("DolphinEmu", "Getting MIME type for URI: " + uri);
String lastSegment = uri.getLastPathSegment();
if (lastSegment == null)
{
Log.e("DolphinEmu", "Badly formatted URI: " + uri);
return null;
}
if (lastSegment.equals(GameDatabase.TABLE_NAME_FOLDERS))
{
return MIME_TYPE_FOLDER;
}
else if (lastSegment.equals(GameDatabase.TABLE_NAME_GAMES))
{
return MIME_TYPE_GAME;
}
Log.e("DolphinEmu", "Unknown MIME type for URI: " + uri);
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values)
{
Log.i("DolphinEmu", "Inserting row at URI: " + uri);
SQLiteDatabase database = mDbHelper.getWritableDatabase();
String table = uri.getLastPathSegment();
long id = -1;
if (table != null)
{
id = database.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_IGNORE);
// If insertion was successful...
if (id > 0)
{
// If we just added a folder, add its contents to the game list.
if (table.equals(GameDatabase.TABLE_NAME_FOLDERS))
{
mDbHelper.scanLibrary(database);
}
// Notify the UI that its contents should be refreshed.
getContext().getContentResolver().notifyChange(uri, null);
uri = Uri.withAppendedPath(uri, Long.toString(id));
}
else
{
Log.e("DolphinEmu", "Row already exists: " + uri + " id: " + id);
}
}
else
{
Log.e("DolphinEmu", "Badly formatted URI: " + uri);
}
database.close();
return uri;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs)
{
Log.e("DolphinEmu", "Delete operations unsupported. URI: " + uri);
return 0;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
{
Log.e("DolphinEmu", "Update operations unsupported. URI: " + uri);
return 0;
}
}

View File

@ -7,7 +7,10 @@ import android.widget.TextView;
import org.dolphinemu.dolphinemu.R; import org.dolphinemu.dolphinemu.R;
/**
* A simple class that stores references to views so that the FileAdapter doesn't need to
* keep calling findViewById(), which is expensive.
*/
public class FileViewHolder extends RecyclerView.ViewHolder public class FileViewHolder extends RecyclerView.ViewHolder
{ {
public View itemView; public View itemView;

View File

@ -6,19 +6,26 @@ import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import org.dolphinemu.dolphinemu.R; import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.model.Game;
/**
* A simple class that stores references to views so that the GameAdapter doesn't need to
* keep calling findViewById(), which is expensive.
*/
public class GameViewHolder extends RecyclerView.ViewHolder public class GameViewHolder extends RecyclerView.ViewHolder
{ {
public ImageView imageScreenshot; public ImageView imageScreenshot;
public TextView textGameTitle; public TextView textGameTitle;
public TextView textCompany; public TextView textCompany;
// Used to handle onClick(). Set this in onBindViewHolder(). public String gameId;
// TODO Not need any of this stuff. Currently only the properties dialog needs it.
public String path; public String path;
public String title;
public String description;
public int country;
public String company;
public String screenshotPath; public String screenshotPath;
public Game game;
public GameViewHolder(View itemView) public GameViewHolder(View itemView)
{ {

View File

@ -21,7 +21,7 @@
#include <android/log.h> #include <android/log.h>
#include <android/native_window_jni.h> #include <android/native_window_jni.h>
#include <EGL/egl.h> #include <EGL/egl.h>
#include "../DiscIO/Volume.h"
#include "Android/ButtonManager.h" #include "Android/ButtonManager.h"
#include "Common/CommonPaths.h" #include "Common/CommonPaths.h"
#include "Common/CommonTypes.h" #include "Common/CommonTypes.h"
@ -43,7 +43,6 @@
#include "VideoCommon/OnScreenDisplay.h" #include "VideoCommon/OnScreenDisplay.h"
#include "VideoCommon/VideoBackendBase.h" #include "VideoCommon/VideoBackendBase.h"
#include "../DiscIO/Volume.h"
ANativeWindow* surf; ANativeWindow* surf;
std::string g_filename; std::string g_filename;
@ -329,7 +328,8 @@ static u64 GetFileSize(std::string filename)
if (pVolume != nullptr) if (pVolume != nullptr)
{ {
u64 size = pVolume->GetSize(); u64 size = pVolume->GetSize();
__android_log_print(ANDROID_LOG_INFO, DOLPHIN_TAG, "Size: %lu", size); // Causes a warning because size is u64, not 'long unsigned'
//__android_log_print(ANDROID_LOG_INFO, DOLPHIN_TAG, "Size: %lu", size);
return size; return size;
} }