summaryrefslogtreecommitdiff
path: root/Source/Android/app/src/main/java
diff options
context:
space:
mode:
authorJules Blok <jules.blok@gmail.com>2015-06-10 00:00:12 +0200
committerJules Blok <jules.blok@gmail.com>2015-06-10 00:00:12 +0200
commit7dfced21a2e5aac7195e0e1ad76468e30306766b (patch)
treebd671b639d3d911460b767caabad8fc8a759a6bf /Source/Android/app/src/main/java
parentc25be031fc1031d795157d8344b87b7841145197 (diff)
parent7b0a65e295c784f9d573f2ef52d4e2a7b9bb2889 (diff)
Merge branch 'master' into stable
Diffstat (limited to 'Source/Android/app/src/main/java')
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java282
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/AddDirectoryActivity.java136
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java334
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/GameGridActivity.java227
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/SettingsActivity.java41
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/FileAdapter.java212
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.java288
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.java102
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/MotionAlertDialog.java184
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.java222
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/SettingsFragment.java197
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/FileListItem.java95
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/Game.java154
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameDatabase.java240
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameProvider.java149
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.java290
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableButton.java45
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableJoystick.java137
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/AssetCopyService.java124
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/SettingsSaveService.java30
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CPUHelper.java395
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/EGLHelper.java393
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/InputBindingPreference.java66
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/SliderPreference.java94
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/UserPreferences.java284
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/FileViewHolder.java30
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/GameViewHolder.java40
27 files changed, 4791 insertions, 0 deletions
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java
new file mode 100644
index 0000000000..d8b1b6c112
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright 2013 Dolphin Emulator Project
+ * Licensed under GPLv2+
+ * Refer to the license.txt file included.
+ */
+
+package org.dolphinemu.dolphinemu;
+
+import android.util.Log;
+import android.view.Surface;
+import android.widget.Toast;
+
+import org.dolphinemu.dolphinemu.activities.EmulationActivity;
+
+/**
+ * Class which contains methods that interact
+ * with the native side of the Dolphin code.
+ */
+public final class NativeLibrary
+{
+ private static EmulationActivity mEmulationActivity;
+
+ /**
+ * Button type for use in onTouchEvent
+ */
+ public static final class ButtonType
+ {
+ public static final int BUTTON_A = 0;
+ public static final int BUTTON_B = 1;
+ public static final int BUTTON_START = 2;
+ public static final int BUTTON_X = 3;
+ public static final int BUTTON_Y = 4;
+ public static final int BUTTON_Z = 5;
+ public static final int BUTTON_UP = 6;
+ public static final int BUTTON_DOWN = 7;
+ public static final int BUTTON_LEFT = 8;
+ public static final int BUTTON_RIGHT = 9;
+ public static final int STICK_MAIN = 10;
+ public static final int STICK_MAIN_UP = 11;
+ public static final int STICK_MAIN_DOWN = 12;
+ public static final int STICK_MAIN_LEFT = 13;
+ public static final int STICK_MAIN_RIGHT = 14;
+ public static final int STICK_C = 15;
+ public static final int STICK_C_UP = 16;
+ public static final int STICK_C_DOWN = 17;
+ public static final int STICK_C_LEFT = 18;
+ public static final int STICK_C_RIGHT = 19;
+ public static final int TRIGGER_L = 20;
+ public static final int TRIGGER_R = 21;
+ }
+
+ /**
+ * Button states
+ */
+ public static final class ButtonState
+ {
+ public static final int RELEASED = 0;
+ public static final int PRESSED = 1;
+ }
+
+ private NativeLibrary()
+ {
+ // Disallows instantiation.
+ }
+
+ /**
+ * Default touchscreen device
+ */
+ public static final String TouchScreenDevice = "Touchscreen";
+
+ /**
+ * Handles button press events for a gamepad.
+ *
+ * @param Device The input descriptor of the gamepad.
+ * @param Button Key code identifying which button was pressed.
+ * @param Action Mask identifying which action is happing (button pressed down, or button released).
+ *
+ * @return If we handled the button press.
+ */
+ public static native boolean onGamePadEvent(String Device, int Button, int Action);
+
+ /**
+ * Handles gamepad movement events.
+ *
+ * @param Device The device ID of the gamepad.
+ * @param Axis The axis ID
+ * @param Value The value of the axis represented by the given ID.
+ */
+ public static native void onGamePadMoveEvent(String Device, int Axis, float Value);
+
+ /**
+ * Gets a value from a key in the given ini-based config file.
+ *
+ * @param configFile The ini-based config file to get the value from.
+ * @param Section The section key that the actual key is in.
+ * @param Key The key to get the value from.
+ * @param Default The value to return in the event the given key doesn't exist.
+ *
+ * @return the value stored at the key, or a default value if it doesn't exist.
+ */
+ public static native String GetConfig(String configFile, String Section, String Key, String Default);
+
+ /**
+ * Sets a value to a key in the given ini config file.
+ *
+ * @param configFile The ini-based config file to add the value to.
+ * @param Section The section key for the ini key
+ * @param Key The actual ini key to set.
+ * @param Value The string to set the ini key to.
+ */
+ public static native void SetConfig(String configFile, String Section, String Key, String Value);
+
+ /**
+ * Sets the filename to be run during emulation.
+ *
+ * @param filename The filename to be run during emulation.
+ */
+ public static native void SetFilename(String filename);
+
+ /**
+ * Gets the embedded banner within the given ISO/ROM.
+ *
+ * @param filename the file path to the ISO/ROM.
+ *
+ * @return an integer array containing the color data for the banner.
+ */
+ public static native int[] GetBanner(String filename);
+
+ /**
+ * Gets the embedded title of the given ISO/ROM.
+ *
+ * @param filename The file path to the ISO/ROM.
+ *
+ * @return the embedded title of the ISO/ROM.
+ */
+ public static native String GetTitle(String filename);
+
+ public static native String GetDescription(String filename);
+ public static native String GetGameId(String filename);
+
+ public static native int GetCountry(String filename);
+
+ public static native String GetCompany(String filename);
+ public static native long GetFilesize(String filename);
+
+ public static native int GetPlatform(String filename);
+
+ /**
+ * Gets the Dolphin version string.
+ *
+ * @return the Dolphin version string.
+ */
+ public static native String GetVersionString();
+
+ /**
+ * Returns if the phone supports NEON or not
+ *
+ * @return true if it supports NEON, false otherwise.
+ */
+ public static native boolean SupportsNEON();
+
+ /**
+ * Saves a screen capture of the game
+ *
+ */
+ public static native void SaveScreenShot();
+
+ /**
+ * Saves a game state to the slot number.
+ *
+ * @param slot The slot location to save state to.
+ */
+ public static native void SaveState(int slot);
+
+ /**
+ * Loads a game state from the slot number.
+ *
+ * @param slot The slot location to load state from.
+ */
+ public static native void LoadState(int slot);
+
+ /**
+ * Creates the initial folder structure in /sdcard/dolphin-emu/
+ */
+ public static native void CreateUserFolders();
+
+ /**
+ * Sets the current working user directory
+ * If not set, it auto-detects a location
+ */
+ public static native void SetUserDirectory(String directory);
+
+ /**
+ * Returns the current working user directory
+ */
+ public static native String GetUserDirectory();
+
+ /**
+ * Begins emulation.
+ *
+ * @param surf The surface to render to.
+ */
+ public static native void Run(Surface surf);
+
+ /** Unpauses emulation from a paused state. */
+ public static native void UnPauseEmulation();
+
+ /** Pauses emulation. */
+ public static native void PauseEmulation();
+
+ /** Stops emulation. */
+ public static native void StopEmulation();
+
+ /**
+ * Enables or disables CPU block profiling
+ * @param enable
+ */
+ public static native void SetProfiling(boolean enable);
+
+ /**
+ * Writes out the block profile results
+ */
+ public static native void WriteProfileResults();
+
+ /**
+ * @return If we have an alert
+ */
+ public static native boolean HasAlertMsg();
+
+ /**
+ * @return The alert string
+ */
+ public static native String GetAlertMsg();
+
+ /**
+ * Clears event in the JNI so we can continue onward
+ */
+ public static native void ClearAlertMsg();
+
+ /** Native EGL functions not exposed by Java bindings **/
+ public static native void eglBindAPI(int api);
+
+ /**
+ * The methods C++ uses to find references to Java classes and methods
+ * are really expensive. Rather than calling them every time we want to
+ * run them, do it once when we load the native library.
+ */
+ private static native void CacheClassesAndMethods();
+
+ static
+ {
+ try
+ {
+ System.loadLibrary("main");
+ }
+ catch (UnsatisfiedLinkError ex)
+ {
+ Log.e("NativeLibrary", ex.toString());
+ }
+
+ CacheClassesAndMethods();
+ }
+
+ public static void displayAlertMsg(final String alert)
+ {
+ Log.e("DolphinEmu", "Alert: " + alert);
+ mEmulationActivity.runOnUiThread(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ Toast.makeText(mEmulationActivity, "Panic Alert: " + alert, Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+
+ public static void setEmulationActivity(EmulationActivity emulationActivity)
+ {
+ Log.v("DolphinEmu", "Registering EmulationActivity.");
+ mEmulationActivity = emulationActivity;
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/AddDirectoryActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/AddDirectoryActivity.java
new file mode 100644
index 0000000000..1cbc30e7e5
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/AddDirectoryActivity.java
@@ -0,0 +1,136 @@
+package org.dolphinemu.dolphinemu.activities;
+
+import android.app.Activity;
+import android.content.AsyncQueryHandler;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.widget.Toolbar;
+
+import org.dolphinemu.dolphinemu.BuildConfig;
+import org.dolphinemu.dolphinemu.R;
+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)
+ * contains the user's games.
+ */
+public class AddDirectoryActivity extends Activity implements FileAdapter.FileClickListener
+{
+ public static final String KEY_CURRENT_PATH = BuildConfig.APPLICATION_ID + ".path";
+
+ private FileAdapter mAdapter;
+ private Toolbar mToolbar;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_add_directory);
+
+ mToolbar = (Toolbar) findViewById(R.id.toolbar_folder_list);
+ setActionBar(mToolbar);
+
+ RecyclerView recyclerView = (RecyclerView) findViewById(R.id.list_files);
+
+ // Specifying the LayoutManager determines how the RecyclerView arranges views.
+ RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
+ recyclerView.setLayoutManager(layoutManager);
+
+ String path;
+ // Stuff in this block only happens when this activity is newly created (i.e. not a rotation)
+ if (savedInstanceState == null)
+ {
+ path = Environment.getExternalStorageDirectory().getPath();
+ }
+ else
+ {
+ // Get the path we were looking at before we rotated.
+ path = savedInstanceState.getString(KEY_CURRENT_PATH);
+ }
+
+ mAdapter = new FileAdapter(path, this);
+ recyclerView.setAdapter(mAdapter);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu)
+ {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.menu_add_directory, menu);
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item)
+ {
+ switch (item.getItemId())
+ {
+ case R.id.menu_up_one_level:
+ mAdapter.upOneLevel();
+ break;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState)
+ {
+ super.onSaveInstanceState(outState);
+
+ // Save the path we're looking at so when rotation is done, we start from same folder.
+ outState.putString(KEY_CURRENT_PATH, mAdapter.getPath());
+ }
+
+ /**
+ * Add a directory to the library, and if successful, end the activity.
+ *
+ * @param path The target directory's path.
+ */
+ @Override
+ public void addDirectory()
+ {
+ // 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());
+ setResult(RESULT_OK, resultData);
+
+ 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
+ public void updateSubtitle(String path)
+ {
+ mToolbar.setSubtitle(path);
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java
new file mode 100644
index 0000000000..e1804abf96
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java
@@ -0,0 +1,334 @@
+package org.dolphinemu.dolphinemu.activities;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+
+import org.dolphinemu.dolphinemu.NativeLibrary;
+import org.dolphinemu.dolphinemu.R;
+import org.dolphinemu.dolphinemu.fragments.EmulationFragment;
+
+import java.util.List;
+
+public final class EmulationActivity extends Activity
+{
+ private View mDecorView;
+
+ private boolean mDeviceHasTouchScreen;
+ private boolean mSystemUiVisible;
+
+ /**
+ * Handlers are a way to pass a message to an Activity telling it to do something
+ * on the UI thread. This Handler responds to any message, even blank ones, by
+ * hiding the system UI.
+ */
+ private Handler mSystemUiHider = new Handler()
+ {
+ @Override
+ public void handleMessage(Message msg)
+ {
+ hideSystemUI();
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+
+ mDeviceHasTouchScreen = getPackageManager().hasSystemFeature("android.hardware.touchscreen");
+
+ // Get a handle to the Window containing the UI.
+ mDecorView = getWindow().getDecorView();
+
+ // Set these options now so that the SurfaceView the game renders into is the right size.
+ mDecorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
+ View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+
+ // Set the ActionBar to follow the navigation/status bar's visibility changes.
+ mDecorView.setOnSystemUiVisibilityChangeListener(
+ new View.OnSystemUiVisibilityChangeListener()
+ {
+ @Override
+ public void onSystemUiVisibilityChange(int flags)
+ {
+ mSystemUiVisible = (flags & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0;
+
+ if (mSystemUiVisible)
+ {
+ getActionBar().show();
+ hideSystemUiAfterDelay();
+ }
+ else
+ {
+ getActionBar().hide();
+ }
+ }
+ }
+ );
+
+ setContentView(R.layout.activity_emulation);
+
+ Intent gameToEmulate = getIntent();
+ String path = gameToEmulate.getStringExtra("SelectedGame");
+ String title = gameToEmulate.getStringExtra("SelectedTitle");
+
+ setTitle(title);
+
+ // Instantiate an EmulationFragment.
+ EmulationFragment emulationFragment = EmulationFragment.newInstance(path);
+
+ // Add fragment to the activity - this triggers all its lifecycle callbacks.
+ getFragmentManager().beginTransaction()
+ .add(R.id.frame_content, emulationFragment, EmulationFragment.FRAGMENT_TAG)
+ .commit();
+ }
+
+ @Override
+ protected void onStart()
+ {
+ super.onStart();
+ Log.d("DolphinEmu", "EmulationActivity starting.");
+ NativeLibrary.setEmulationActivity(this);
+ }
+
+ @Override
+ protected void onStop()
+ {
+ super.onStop();
+ Log.d("DolphinEmu", "EmulationActivity stopping.");
+
+ NativeLibrary.setEmulationActivity(null);
+ }
+
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState)
+ {
+ super.onPostCreate(savedInstanceState);
+
+ // Give the user a few seconds to see what the controls look like, then hide them.
+ hideSystemUiAfterDelay();
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus)
+ {
+ super.onWindowFocusChanged(hasFocus);
+
+ if (hasFocus)
+ {
+ hideSystemUiAfterDelay();
+ }
+ else
+ {
+ // If the window loses focus (i.e. a dialog box, or a popup menu is on screen
+ // stop hiding the UI.
+ mSystemUiHider.removeMessages(0);
+ }
+ }
+
+ @Override
+ public void onBackPressed()
+ {
+ if (!mDeviceHasTouchScreen && !mSystemUiVisible)
+ {
+ showSystemUI();
+ }
+ else
+ {
+ // Let the system handle it; i.e. quit the activity TODO or show "are you sure?" dialog.
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu)
+ {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.menu_emulation, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item)
+ {
+ switch (item.getItemId())
+ {
+ // Enable/Disable input overlay.
+ case R.id.enableInputOverlay:
+ {
+ EmulationFragment emulationFragment = (EmulationFragment) getFragmentManager()
+ .findFragmentByTag(EmulationFragment.FRAGMENT_TAG);
+
+ emulationFragment.toggleInputOverlayVisibility();
+
+ return true;
+ }
+
+ // Screenshot capturing
+ case R.id.takeScreenshot:
+ NativeLibrary.SaveScreenShot();
+ return true;
+
+ // Save state slots
+ case R.id.saveSlot1:
+ NativeLibrary.SaveState(0);
+ return true;
+
+ case R.id.saveSlot2:
+ NativeLibrary.SaveState(1);
+ return true;
+
+ case R.id.saveSlot3:
+ NativeLibrary.SaveState(2);
+ return true;
+
+ case R.id.saveSlot4:
+ NativeLibrary.SaveState(3);
+ return true;
+
+ case R.id.saveSlot5:
+ NativeLibrary.SaveState(4);
+ return true;
+
+ // Load state slots
+ case R.id.loadSlot1:
+ NativeLibrary.LoadState(0);
+ return true;
+
+ case R.id.loadSlot2:
+ NativeLibrary.LoadState(1);
+ return true;
+
+ case R.id.loadSlot3:
+ NativeLibrary.LoadState(2);
+ return true;
+
+ case R.id.loadSlot4:
+ NativeLibrary.LoadState(3);
+ return true;
+
+ case R.id.loadSlot5:
+ NativeLibrary.LoadState(4);
+ return true;
+
+ case R.id.exitEmulation:
+ {
+ // Create a confirmation method for quitting the current emulation instance.
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.overlay_exit_emulation);
+ builder.setMessage(R.string.overlay_exit_emulation_confirm);
+ builder.setNegativeButton(R.string.no, null);
+ builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener()
+ {
+ public void onClick(DialogInterface dialog, int which)
+ {
+ onDestroy();
+ }
+ });
+ builder.show();
+ return true;
+ }
+
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ // Gets button presses
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event)
+ {
+ int action = 0;
+
+ switch (event.getAction())
+ {
+ case KeyEvent.ACTION_DOWN:
+ // Handling the case where the back button is pressed.
+ if (event.getKeyCode() == KeyEvent.KEYCODE_BACK)
+ {
+ onBackPressed();
+ return true;
+ }
+
+ // Normal key events.
+ action = NativeLibrary.ButtonState.PRESSED;
+ break;
+ case KeyEvent.ACTION_UP:
+ action = NativeLibrary.ButtonState.RELEASED;
+ break;
+ default:
+ return false;
+ }
+ InputDevice input = event.getDevice();
+ boolean handled = NativeLibrary.onGamePadEvent(input.getDescriptor(), event.getKeyCode(), action);
+ return handled;
+ }
+
+ @Override
+ public boolean dispatchGenericMotionEvent(MotionEvent event)
+ {
+ if (((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0))
+ {
+ return super.dispatchGenericMotionEvent(event);
+ }
+
+ // Don't attempt to do anything if we are disconnecting a device.
+ if (event.getActionMasked() == MotionEvent.ACTION_CANCEL)
+ return true;
+
+ InputDevice input = event.getDevice();
+ List<InputDevice.MotionRange> motions = input.getMotionRanges();
+
+ for (InputDevice.MotionRange range : motions)
+ {
+ NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), range.getAxis(), event.getAxisValue(range.getAxis()));
+ }
+
+ return true;
+ }
+
+ private void hideSystemUiAfterDelay()
+ {
+ // Clear any pending hide events.
+ mSystemUiHider.removeMessages(0);
+
+ // Add a new hide event, to occur 3 seconds from now.
+ mSystemUiHider.sendEmptyMessageDelayed(0, 3000);
+ }
+
+ private void hideSystemUI()
+ {
+ mSystemUiVisible = false;
+
+ mDecorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
+ View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_FULLSCREEN |
+ View.SYSTEM_UI_FLAG_IMMERSIVE);
+ }
+
+ private void showSystemUI()
+ {
+ mSystemUiVisible = true;
+
+ mDecorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
+ View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+
+ hideSystemUiAfterDelay();
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/GameGridActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/GameGridActivity.java
new file mode 100644
index 0000000000..cbb4ab8058
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/GameGridActivity.java
@@ -0,0 +1,227 @@
+package org.dolphinemu.dolphinemu.activities;
+
+import android.app.Activity;
+import android.app.LoaderManager;
+import android.content.CursorLoader;
+import android.content.Intent;
+import android.content.Loader;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.Toolbar;
+
+import org.dolphinemu.dolphinemu.NativeLibrary;
+import org.dolphinemu.dolphinemu.R;
+import org.dolphinemu.dolphinemu.adapters.GameAdapter;
+import org.dolphinemu.dolphinemu.model.GameDatabase;
+import org.dolphinemu.dolphinemu.model.GameProvider;
+import org.dolphinemu.dolphinemu.services.AssetCopyService;
+
+/**
+ * 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.
+ */
+public final class GameGridActivity extends Activity implements LoaderManager.LoaderCallbacks<Cursor>
+{
+ 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;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_game_grid);
+
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar_game_list);
+ setActionBar(toolbar);
+
+ ImageButton buttonAddDirectory = (ImageButton) findViewById(R.id.button_add_directory);
+ RecyclerView recyclerView = (RecyclerView) findViewById(R.id.grid_games);
+
+ // TODO Rather than calling into native code, this should use the commented line below.
+ // String versionName = BuildConfig.VERSION_NAME;
+ String versionName = NativeLibrary.GetVersionString();
+ toolbar.setSubtitle(versionName);
+
+ // Specifying the LayoutManager determines how the RecyclerView arranges views.
+ RecyclerView.LayoutManager layoutManager = new GridLayoutManager(this,
+ getResources().getInteger(R.integer.game_grid_columns));
+ recyclerView.setLayoutManager(layoutManager);
+
+ recyclerView.addItemDecoration(new GameAdapter.SpacesItemDecoration(8));
+
+ // Create an adapter that will relate the dataset to the views on-screen.
+ getLoaderManager().initLoader(LOADER_ID_GAMES, null, this);
+ mAdapter = new GameAdapter();
+ recyclerView.setAdapter(mAdapter);
+
+ buttonAddDirectory.setOnClickListener(new View.OnClickListener()
+ {
+ @Override
+ public void onClick(View view)
+ {
+ Intent fileChooser = new Intent(GameGridActivity.this, AddDirectoryActivity.class);
+
+ // The second argument to this method is read below in onActivityResult().
+ startActivityForResult(fileChooser, REQUEST_ADD_DIRECTORY);
+ }
+ });
+
+ // Stuff in this block only happens when this activity is newly created (i.e. not a rotation)
+ if (savedInstanceState == null)
+ {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
+ boolean assetsCopied = preferences.getBoolean("assetsCopied", false);
+
+ // Only perform these extensive copy operations once.
+ if (!assetsCopied)
+ {
+ // Copy assets into appropriate locations.
+ Intent copyAssets = new Intent(this, AssetCopyService.class);
+ startService(copyAssets);
+ }
+ }
+ }
+
+ /**
+ * Callback from AddDirectoryActivity. Applies any changes necessary to the GameGridActivity.
+ *
+ * @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.
+ * @param result The information the returning Activity is providing us.
+ */
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent result)
+ {
+ // If the user picked a file, as opposed to just backing out.
+ if (resultCode == RESULT_OK)
+ {
+ // Sanity check to make sure the Activity that just returned was the AddDirectoryActivity;
+ // other activities might use this callback in the future (don't forget to change Javadoc!)
+ if (requestCode == REQUEST_ADD_DIRECTORY)
+ {
+ getLoaderManager().restartLoader(LOADER_ID_GAMES, null, this);
+ }
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu)
+ {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.menu_game_grid, menu);
+ return true;
+ }
+
+ /**
+ * Called by the framework whenever any actionbar/toolbar icon is clicked.
+ *
+ * @param item The icon that was clicked on.
+ * @return True if the event was handled, false to bubble it up to the OS.
+ */
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item)
+ {
+ switch (item.getItemId())
+ {
+ case R.id.menu_settings:
+ // Launch the Settings Actvity.
+ Intent settings = new Intent(this, SettingsActivity.class);
+ startActivity(settings);
+ return true;
+
+ case R.id.menu_refresh:
+ getContentResolver().insert(GameProvider.URI_REFRESH, null);
+ getLoaderManager().restartLoader(LOADER_ID_GAMES, null, this);
+
+ return true;
+ }
+
+ return false;
+ }
+
+
+ /**
+ * 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)
+ {
+ Log.d("DolphinEmu", "Creating loader with id: " + id);
+
+ // Take action based on the ID of the Loader that's being created.
+ switch (id)
+ {
+ case LOADER_ID_GAMES:
+ // TODO Play some sort of load-starting animation; maybe fade the list out.
+
+ return new CursorLoader(
+ this, // Parent activity context
+ GameProvider.URI_GAME, // URI of table to query
+ null, // Return all columns
+ null, // No selection clause
+ null, // No selection arguments
+ GameDatabase.KEY_GAME_TITLE + " asc" // Sort by game name, ascending order
+ );
+
+ default:
+ Log.e("DolphinEmu", "Bad ID passed in.");
+ return null;
+ }
+ }
+
+ /**
+ * 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.");
+ }
+
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader)
+ {
+ Log.d("DolphinEmu", "Loader resetting.");
+
+ // TODO ¯\_(ツ)_/¯
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/SettingsActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/SettingsActivity.java
new file mode 100644
index 0000000000..fe1ddd14d4
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/SettingsActivity.java
@@ -0,0 +1,41 @@
+package org.dolphinemu.dolphinemu.activities;
+
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import org.dolphinemu.dolphinemu.fragments.SettingsFragment;
+import org.dolphinemu.dolphinemu.services.SettingsSaveService;
+
+public final class SettingsActivity extends Activity
+{
+ @Override
+ protected void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+
+ // Display the fragment as the main content.
+ getFragmentManager().beginTransaction()
+ .replace(android.R.id.content, new SettingsFragment(), "settings_fragment")
+ .commit();
+ }
+
+ /**
+ * If this is called, the user has left the settings screen (potentially through the
+ * home button) and will expect their changes to be persisted. So we kick off an
+ * IntentService which will do so on a background thread.
+ */
+ @Override
+ protected void onStop()
+ {
+ super.onStop();
+
+ Log.d("DolphinEmulator", "Settings activity stopping. Saving settings to INI...");
+
+ // Copy assets into appropriate locations.
+ Intent settingsSaver = new Intent(this, SettingsSaveService.class);
+ startService(settingsSaver);
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/FileAdapter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/FileAdapter.java
new file mode 100644
index 0000000000..8d07b6c101
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/FileAdapter.java
@@ -0,0 +1,212 @@
+package org.dolphinemu.dolphinemu.adapters;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import org.dolphinemu.dolphinemu.R;
+import org.dolphinemu.dolphinemu.model.FileListItem;
+import org.dolphinemu.dolphinemu.viewholders.FileViewHolder;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+
+public final class FileAdapter extends RecyclerView.Adapter<FileViewHolder> implements View.OnClickListener
+{
+ private ArrayList<FileListItem> mFileList;
+
+ private String mPath;
+
+ private FileClickListener mListener;
+
+ /**
+ * Initializes the dataset to be displayed, and associates the Adapter with the
+ * Activity as an event listener.
+ *
+ * @param path A String containing the path to the directory to be shown by this Adapter.
+ * @param listener An Activity that can respond to callbacks from this Adapter.
+ */
+ public FileAdapter(String path, FileClickListener listener)
+ {
+ mFileList = generateFileList(new File(path));
+ mListener = listener;
+ mListener.updateSubtitle(path);
+ }
+
+ /**
+ * Called by the LayoutManager when it is necessary to create a new view.
+ *
+ * @param parent The RecyclerView (I think?) the created view will be thrown into.
+ * @param viewType Not used here, but useful when more than one type of child will be used in the RecyclerView.
+ * @return The created ViewHolder with references to all the child view's members.
+ */
+ @Override
+ public FileViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
+ {
+ // Create a new view.
+ View listItem = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.list_item_file, parent, false);
+
+ listItem.setOnClickListener(this);
+
+ // Use that view to create a ViewHolder.
+ return new FileViewHolder(listItem);
+ }
+
+ /**
+ * Called by the LayoutManager when a new view is not necessary because we can recycle
+ * an existing one (for example, if a view just scrolled onto the screen from the bottom, we
+ * can use the view that just scrolled off the top instead of inflating a new one.)
+ *
+ * @param holder A ViewHolder representing the view we're recycling.
+ * @param position The position of the 'new' view in the dataset.
+ */
+ @Override
+ public void onBindViewHolder(FileViewHolder holder, int position)
+ {
+ // Get a reference to the item from the dataset; we'll use this to fill in the view contents.
+ final FileListItem file = mFileList.get(position);
+
+ // Fill in the view contents.
+ switch (file.getType())
+ {
+ case FileListItem.TYPE_FOLDER:
+ holder.imageType.setImageResource(R.drawable.ic_folder);
+ break;
+
+ case FileListItem.TYPE_GC:
+ holder.imageType.setImageResource(R.drawable.ic_gamecube);
+ break;
+
+ case FileListItem.TYPE_WII:
+ holder.imageType.setImageResource(R.drawable.ic_wii);
+ break;
+
+ case FileListItem.TYPE_OTHER:
+ holder.imageType.setImageResource(android.R.color.transparent);
+ break;
+ }
+
+ holder.textFileName.setText(file.getFilename());
+ holder.itemView.setTag(file.getPath());
+ }
+
+ /**
+ * Called by the LayoutManager to find out how much data we have.
+ *
+ * @return Size of the dataset.
+ */
+ @Override
+ public int getItemCount()
+ {
+ return mFileList.size();
+ }
+
+ /**
+ * When a file is clicked, determine if it is a directory; if it is, show that new directory's
+ * contents. If it is not, end the activity successfully.
+ *
+ * @param view The View representing the file the user clicked on.
+ */
+ @Override
+ public void onClick(final View view)
+ {
+ final String path = (String) view.getTag();
+
+ File clickedFile = new File(path);
+
+ if (clickedFile.isDirectory())
+ {
+ final ArrayList<FileListItem> fileList = generateFileList(clickedFile);
+
+ if (fileList.isEmpty())
+ {
+ Toast.makeText(view.getContext(), R.string.add_directory_empty_folder, Toast.LENGTH_SHORT).show();
+ }
+ else
+ {
+ // Delay the loading of the new directory to give a little bit of time for UI feedback
+ // to happen. Hacky, but good enough for now; this is necessary because we're modifying
+ // the RecyclerView's contents, rather than constructing a new one.
+ view.getHandler().postDelayed(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ mFileList = fileList;
+ notifyDataSetChanged();
+ mListener.updateSubtitle(path);
+ }
+ }, 200);
+ }
+ }
+ else
+ {
+ // Pass the activity the path of the parent directory of the clicked file.
+ mListener.addDirectory();
+ }
+ }
+
+ /**
+ * For a given directory, return a list of Files it contains.
+ *
+ * @param directory A File representing the directory that should have its contents displayed.
+ * @return The list of files contained in the directory.
+ */
+ private ArrayList<FileListItem> generateFileList(File directory)
+ {
+ File[] children = directory.listFiles();
+ ArrayList<FileListItem> fileList = new ArrayList<FileListItem>(children.length);
+
+ for (File child : children)
+ {
+ if (!child.isHidden())
+ {
+ FileListItem item = new FileListItem(child);
+ fileList.add(item);
+ }
+ }
+
+ mPath = directory.getAbsolutePath();
+
+ Collections.sort(fileList);
+ return fileList;
+ }
+
+ public String getPath()
+ {
+ return mPath;
+ }
+
+ public void setPath(String path)
+ {
+ File directory = new File(path);
+
+ mFileList = generateFileList(directory);
+ notifyDataSetChanged();
+ mListener.updateSubtitle(path);
+ }
+
+ public void upOneLevel()
+ {
+ File currentDirectory = new File(mPath);
+ File parentDirectory = currentDirectory.getParentFile();
+
+ mFileList = generateFileList(parentDirectory);
+ notifyDataSetChanged();
+ mListener.updateSubtitle(mPath);
+ }
+
+ /**
+ * Callback to the containing Activity.
+ */
+ public interface FileClickListener
+ {
+ void addDirectory();
+
+ void updateSubtitle(String path);
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.java
new file mode 100644
index 0000000000..d296c5e023
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.java
@@ -0,0 +1,288 @@
+package org.dolphinemu.dolphinemu.adapters;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.graphics.Rect;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.squareup.picasso.Picasso;
+
+import org.dolphinemu.dolphinemu.R;
+import org.dolphinemu.dolphinemu.activities.EmulationActivity;
+import org.dolphinemu.dolphinemu.dialogs.GameDetailsDialog;
+import org.dolphinemu.dolphinemu.model.GameDatabase;
+import org.dolphinemu.dolphinemu.viewholders.GameViewHolder;
+
+/**
+ * This adapter, unlike {@link FileAdapter} which is backed by an ArrayList, gets its
+ * 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.OnLongClickListener
+{
+ private Cursor mCursor;
+ private GameDataSetObserver mObserver;
+
+ private boolean mDatasetValid;
+
+ /**
+ * 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.
+ */
+ public GameAdapter()
+ {
+ mDatasetValid = false;
+ mObserver = new GameDataSetObserver();
+ }
+
+ /**
+ * Called by the LayoutManager when it is necessary to create a new view.
+ *
+ * @param parent The RecyclerView (I think?) the created view will be thrown into.
+ * @param viewType Not used here, but useful when more than one type of child will be used in the RecyclerView.
+ * @return The created ViewHolder with references to all the child view's members.
+ */
+ @Override
+ public GameViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
+ {
+ // Create a new view.
+ View gameCard = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.card_game, parent, false);
+
+ gameCard.setOnClickListener(this);
+ gameCard.setOnLongClickListener(this);
+
+ // Use that view to create a ViewHolder.
+ return new GameViewHolder(gameCard);
+ }
+
+ /**
+ * Called by the LayoutManager when a new view is not necessary because we can recycle
+ * an existing one (for example, if a view just scrolled onto the screen from the bottom, we
+ * can use the view that just scrolled off the top instead of inflating a new one.)
+ *
+ * @param holder A ViewHolder representing the view we're recycling.
+ * @param position The position of the 'new' view in the dataset.
+ */
+ @Override
+ public void onBindViewHolder(GameViewHolder holder, int position)
+ {
+ if (mDatasetValid)
+ {
+ 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.");
+ }
+
+
+ }
+
+ /**
+ * Called by the LayoutManager to find out how much data we have.
+ *
+ * @return Size of the dataset.
+ */
+ @Override
+ public int getItemCount()
+ {
+ 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();
+ }
+
+ /**
+ * Launches the game that was clicked on.
+ *
+ * @param view The card representing the game the user wants to play.
+ */
+ @Override
+ public void onClick(View view)
+ {
+ GameViewHolder holder = (GameViewHolder) view.getTag();
+
+ // Start the emulation activity and send the path of the clicked ISO to it.
+ Intent intent = new Intent(view.getContext(), EmulationActivity.class);
+
+ intent.putExtra("SelectedGame", holder.path);
+ intent.putExtra("SelectedTitle", holder.title);
+
+ view.getContext().startActivity(intent);
+ }
+
+ /**
+ * Launches the details activity for this Game, using an ID stored in the
+ * details button's Tag.
+ *
+ * @param view The Card button that was long-clicked.
+ */
+ @Override
+ public boolean onLongClick(View view)
+ {
+ GameViewHolder holder = (GameViewHolder) view.getTag();
+
+ // Get the ID of the game we want to look at.
+ // TODO This should be all we need to pass in, eventually.
+ // String gameId = (String) holder.gameId;
+
+ Activity activity = (Activity) view.getContext();
+ GameDetailsDialog.newInstance(holder.title,
+ holder.description,
+ holder.country,
+ holder.company,
+ holder.path,
+ holder.screenshotPath).show(activity.getFragmentManager(), "game_details");
+
+ return true;
+ }
+
+ public static class SpacesItemDecoration extends RecyclerView.ItemDecoration
+ {
+ private int space;
+
+ public SpacesItemDecoration(int space)
+ {
+ this.space = space;
+ }
+
+ @Override
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)
+ {
+ outRect.left = space;
+ outRect.right = space;
+ outRect.bottom = space;
+ 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/dialogs/GameDetailsDialog.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.java
new file mode 100644
index 0000000000..04c7253cb5
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.java
@@ -0,0 +1,102 @@
+package org.dolphinemu.dolphinemu.dialogs;
+
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.squareup.picasso.Picasso;
+
+import org.dolphinemu.dolphinemu.BuildConfig;
+import org.dolphinemu.dolphinemu.R;
+import org.dolphinemu.dolphinemu.activities.EmulationActivity;
+
+import de.hdodenhof.circleimageview.CircleImageView;
+
+public final class GameDetailsDialog extends DialogFragment
+{
+ 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_COUNTRY = BuildConfig.APPLICATION_ID + ".game_country";
+ public static final String ARGUMENT_GAME_DATE = BuildConfig.APPLICATION_ID + ".game_date";
+ 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";
+
+ // 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)
+ {
+ GameDetailsDialog fragment = new GameDetailsDialog();
+
+ Bundle arguments = new Bundle();
+ arguments.putString(ARGUMENT_GAME_TITLE, title);
+ arguments.putString(ARGUMENT_GAME_DESCRIPTION, description);
+ arguments.putInt(ARGUMENT_GAME_COUNTRY, country);
+ arguments.putString(ARGUMENT_GAME_DATE, company);
+ arguments.putString(ARGUMENT_GAME_PATH, path);
+ arguments.putString(ARGUMENT_GAME_SCREENSHOT_PATH, screenshotPath);
+ fragment.setArguments(arguments);
+
+ return fragment;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState)
+ {
+ 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);
+
+ TextView textTitle = (TextView) contents.findViewById(R.id.text_game_title);
+ TextView textDescription = (TextView) contents.findViewById(R.id.text_company);
+
+ TextView textCountry = (TextView) contents.findViewById(R.id.text_country);
+ TextView textDate = (TextView) contents.findViewById(R.id.text_date);
+
+ ImageButton buttonLaunch = (ImageButton) contents.findViewById(R.id.button_launch);
+
+ int countryIndex = getArguments().getInt(ARGUMENT_GAME_COUNTRY);
+ String country = getResources().getStringArray(R.array.country_names)[countryIndex];
+
+ textTitle.setText(getArguments().getString(ARGUMENT_GAME_TITLE));
+ textDescription.setText(getArguments().getString(ARGUMENT_GAME_DESCRIPTION));
+ textCountry.setText(country);
+ textDate.setText(getArguments().getString(ARGUMENT_GAME_DATE));
+ buttonLaunch.setOnClickListener(new View.OnClickListener()
+ {
+ @Override
+ public void onClick(View view)
+ {
+ // Start the emulation activity and send the path of the clicked ROM to it.
+ Intent intent = new Intent(view.getContext(), EmulationActivity.class);
+
+ intent.putExtra("SelectedGame", getArguments().getString(ARGUMENT_GAME_PATH));
+ intent.putExtra("SelectedTitle", getArguments().getString(ARGUMENT_GAME_TITLE));
+
+ startActivity(intent);
+ }
+ });
+
+ // Fill in the view contents.
+ Picasso.with(imageGameScreen.getContext())
+ .load(getArguments().getString(ARGUMENT_GAME_SCREENSHOT_PATH))
+ .fit()
+ .centerCrop()
+ .noFade()
+ .noPlaceholder()
+ .into(imageGameScreen);
+
+ circleBanner.setImageResource(R.drawable.no_banner);
+
+ builder.setView(contents);
+ return builder.create();
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/MotionAlertDialog.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/MotionAlertDialog.java
new file mode 100644
index 0000000000..aea1f39df0
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/MotionAlertDialog.java
@@ -0,0 +1,184 @@
+package org.dolphinemu.dolphinemu.dialogs;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.Preference;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import org.dolphinemu.dolphinemu.NativeLibrary;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * {@link AlertDialog} derivative that listens for
+ * motion events from controllers and joysticks.
+ */
+public final class MotionAlertDialog extends AlertDialog
+{
+ // The selected input preference
+ private final Preference inputPref;
+
+ private boolean firstEvent = true;
+ private final ArrayList<Float> m_values = new ArrayList<Float>();
+
+ /**
+ * Constructor
+ *
+ * @param ctx The current {@link Context}.
+ * @param inputPref The Preference to show this dialog for.
+ */
+ public MotionAlertDialog(Context ctx, Preference inputPref)
+ {
+ super(ctx);
+
+ this.inputPref = inputPref;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event)
+ {
+ Log.d("InputConfigFragment", "Received key event: " + event.getAction());
+ switch (event.getAction())
+ {
+ case KeyEvent.ACTION_DOWN:
+ case KeyEvent.ACTION_UP:
+
+ InputDevice input = event.getDevice();
+ saveInput(input, event, null, false);
+
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+
+ // Method that will be called within dispatchGenericMotionEvent
+ // that handles joystick/controller movements.
+ private boolean onMotionEvent(MotionEvent event)
+ {
+ if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0)
+ return false;
+
+ Log.d("InputConfigFragment", "Received motion event: " + event.getAction());
+
+ InputDevice input = event.getDevice();
+ List<InputDevice.MotionRange> motions = input.getMotionRanges();
+ if (firstEvent)
+ {
+ m_values.clear();
+
+ for (InputDevice.MotionRange range : motions)
+ {
+ m_values.add(event.getAxisValue(range.getAxis()));
+ }
+
+ firstEvent = false;
+ }
+ else
+ {
+ for (int a = 0; a < motions.size(); ++a)
+ {
+ InputDevice.MotionRange range = motions.get(a);
+
+ if (m_values.get(a) > (event.getAxisValue(range.getAxis()) + 0.5f))
+ {
+ saveInput(input, null, range, false);
+ }
+ else if (m_values.get(a) < (event.getAxisValue(range.getAxis()) - 0.5f))
+ {
+ saveInput(input, null, range, true);
+ }
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event)
+ {
+ if (onKeyDown(event.getKeyCode(), event))
+ return true;
+
+ return super.dispatchKeyEvent(event);
+ }
+
+ @Override
+ public boolean dispatchGenericMotionEvent(MotionEvent event)
+ {
+ if (onMotionEvent(event))
+ return true;
+
+ return super.dispatchGenericMotionEvent(event);
+ }
+
+ /**
+ * Saves the provided input setting both to the INI file (so native code can use it) and as an
+ * Android preference (so it persists correctly, and is human-readable.)
+ *
+ * @param device Required; the InputDevice from which the input event originated.
+ * @param keyEvent If the event was a button push, this KeyEvent represents it and is required.
+ * @param motionRange If the event was an axis movement, this MotionRange represents it and is required.
+ * @param axisPositive If the event was an axis movement, this boolean indicates the direction and is required.
+ */
+ private void saveInput(InputDevice device, KeyEvent keyEvent, InputDevice.MotionRange motionRange, boolean axisPositive)
+ {
+ String bindStr = null;
+ String uiString = null;
+
+ if (keyEvent != null)
+ {
+ bindStr = "Device '" + device.getDescriptor() + "'-Button " + keyEvent.getKeyCode();
+ uiString = device.getName() + ": Button " + keyEvent.getKeyCode();
+ }
+
+ if (motionRange != null)
+ {
+ if (axisPositive)
+ {
+ bindStr = "Device '" + device.getDescriptor() + "'-Axis " + motionRange.getAxis() + "+";
+ uiString = device.getName() + ": Axis " + motionRange.getAxis() + "+";
+ }
+ else
+ {
+ bindStr = "Device '" + device.getDescriptor() + "'-Axis " + motionRange.getAxis() + "-";
+ uiString = device.getName() + ": Axis " + motionRange.getAxis() + "-";
+ }
+ }
+
+ if (bindStr != null)
+ {
+ NativeLibrary.SetConfig("Dolphin.ini", "Android", inputPref.getKey(), bindStr);
+ }
+ else
+ {
+ Log.e("DolphinEmu", "Failed to save input to INI.");
+ }
+
+
+ if (uiString != null)
+ {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext());
+ SharedPreferences.Editor editor = preferences.edit();
+
+ editor.putString(inputPref.getKey(), uiString);
+ editor.apply();
+
+ inputPref.setSummary(uiString);
+ }
+ else
+ {
+ Log.e("DolphinEmu", "Failed to save input to preference.");
+ }
+
+ dismiss();
+ }
+} \ No newline at end of file
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.java
new file mode 100644
index 0000000000..70dff7237e
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.java
@@ -0,0 +1,222 @@
+package org.dolphinemu.dolphinemu.fragments;
+
+import android.app.Fragment;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.dolphinemu.dolphinemu.BuildConfig;
+import org.dolphinemu.dolphinemu.NativeLibrary;
+import org.dolphinemu.dolphinemu.R;
+import org.dolphinemu.dolphinemu.overlay.InputOverlay;
+
+
+public final class EmulationFragment extends Fragment implements SurfaceHolder.Callback
+{
+ public static final String FRAGMENT_TAG = BuildConfig.APPLICATION_ID + ".emulation_fragment";
+
+ private static final String ARGUMENT_GAME_PATH = BuildConfig.APPLICATION_ID + ".game_path";
+
+ private SharedPreferences mPreferences;
+
+ private SurfaceView mSurfaceView;
+ private Surface mSurface;
+
+ private InputOverlay mInputOverlay;
+
+ private Thread mEmulationThread;
+
+ private String mPath;
+
+ private boolean mEmulationStarted;
+ private boolean mEmulationRunning;
+
+
+ public static EmulationFragment newInstance(String path)
+ {
+ EmulationFragment fragment = new EmulationFragment();
+
+ Bundle arguments = new Bundle();
+ arguments.putString(ARGUMENT_GAME_PATH, path);
+ fragment.setArguments(arguments);
+
+ return fragment;
+ }
+
+ /**
+ * Initialize anything that doesn't depend on the layout / views in here.
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+
+ // So this fragment doesn't restart on configuration changes; i.e. rotation.
+ setRetainInstance(true);
+
+ mPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
+ }
+
+ /**
+ * Initialize the UI and start emulation in here.
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
+ {
+ mPath = getArguments().getString(ARGUMENT_GAME_PATH);
+ NativeLibrary.SetFilename(mPath);
+
+ View contents = inflater.inflate(R.layout.fragment_emulation, container, false);
+
+ mSurfaceView = (SurfaceView) contents.findViewById(R.id.surface_emulation);
+ mInputOverlay = (InputOverlay) contents.findViewById(R.id.surface_input_overlay);
+
+ mSurfaceView.getHolder().addCallback(this);
+
+ // If the input overlay was previously disabled, then don't show it.
+ if (!mPreferences.getBoolean("showInputOverlay", true))
+ {
+ mInputOverlay.setVisibility(View.GONE);
+ }
+
+
+ if (savedInstanceState == null)
+ {
+ mEmulationThread = new Thread(mEmulationRunner);
+ }
+ else
+ {
+ // Likely a rotation occurred.
+ // TODO Pass native code the Surface, which will have been recreated, from surfaceChanged()
+ // TODO Also, write the native code that will get the video backend to accept the new Surface as one of its own.
+ }
+
+ return contents;
+ }
+
+ @Override
+ public void onStart()
+ {
+ super.onStart();
+ startEmulation();
+ }
+
+ @Override
+ public void onStop()
+ {
+ super.onStop();
+ pauseEmulation();
+ }
+
+ @Override
+ public void onDestroyView()
+ {
+ super.onDestroyView();
+ if (getActivity().isFinishing())
+ {
+ NativeLibrary.StopEmulation();
+ }
+ }
+
+ public void toggleInputOverlayVisibility()
+ {
+ SharedPreferences.Editor editor = mPreferences.edit();
+
+ // If the overlay is currently set to INVISIBLE
+ if (!mPreferences.getBoolean("showInputOverlay", false))
+ {
+ // Set it to VISIBLE
+ mInputOverlay.setVisibility(View.VISIBLE);
+ editor.putBoolean("showInputOverlay", true);
+ }
+ else
+ {
+ // Set it to INVISIBLE
+ mInputOverlay.setVisibility(View.GONE);
+ editor.putBoolean("showInputOverlay", false);
+ }
+
+ editor.apply();
+ }
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder)
+ {
+ Log.d("DolphinEmu", "Surface created.");
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height)
+ {
+ Log.d("DolphinEmu", "Surface changed. Resolution: " + width + "x" + height);
+ mSurface = holder.getSurface();
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder)
+ {
+ Log.d("DolphinEmu", "Surface destroyed.");
+
+ if (mEmulationRunning)
+ {
+ pauseEmulation();
+ }
+ }
+
+ private void startEmulation()
+ {
+ if (!mEmulationStarted)
+ {
+ Log.d("DolphinEmu", "Starting emulation thread.");
+
+ mEmulationThread.start();
+ }
+ else
+ {
+ Log.d("DolphinEmu", "Resuming emulation.");
+ NativeLibrary.UnPauseEmulation();
+ }
+
+ mEmulationRunning = true;
+ }
+
+ private void pauseEmulation()
+ {
+ Log.d("DolphinEmu", "Pausing emulation.");
+
+ NativeLibrary.PauseEmulation();
+ mEmulationRunning = false;
+ }
+
+ private Runnable mEmulationRunner = new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ mEmulationRunning = true;
+ mEmulationStarted = true;
+
+ // Loop until onSurfaceCreated succeeds
+ while (mSurface == null)
+ {
+ if (!mEmulationRunning)
+ {
+ // So that if the user quits before this gets a surface, we don't loop infinitely.
+ return;
+ }
+ }
+
+ Log.i("DolphinEmu", "Starting emulation: " + mSurface);
+
+ // Start emulation using the provided Surface.
+ NativeLibrary.Run(mSurface);
+ }
+ };
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/SettingsFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/SettingsFragment.java
new file mode 100644
index 0000000000..8f77e4a72d
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/SettingsFragment.java
@@ -0,0 +1,197 @@
+package org.dolphinemu.dolphinemu.fragments;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.Environment;
+import android.preference.ListPreference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceManager;
+
+import org.dolphinemu.dolphinemu.R;
+import org.dolphinemu.dolphinemu.utils.EGLHelper;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.microedition.khronos.opengles.GL10;
+
+public final class SettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener
+{
+ private SharedPreferences mPreferences;
+ private ListPreference mVideoBackendPreference;
+
+ private final EGLHelper mEglHelper = new EGLHelper(EGLHelper.EGL_OPENGL_ES2_BIT);
+ private final String mVendor = mEglHelper.getGL().glGetString(GL10.GL_VENDOR);
+
+ private final String mVersion = mEglHelper.getGL().glGetString(GL10.GL_VERSION);
+
+ @Override
+ public void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.preferences);
+
+ // TODO Below here is effectively ported from the old VideoSettingsFragment. There is
+ // TODO probably a simpler way to do this, but potentially could require UI discussion/feedback.
+
+ // Setting valid video backends.
+ mVideoBackendPreference = (ListPreference) findPreference("gpuPref");
+ final boolean deviceSupportsGL = mEglHelper.supportsOpenGL();
+ final boolean deviceSupportsGLES3 = mEglHelper.supportsGLES3();
+
+ if (deviceSupportsGL)
+ {
+ mVideoBackendPreference.setEntries(R.array.videoBackendEntriesGL);
+ mVideoBackendPreference.setEntryValues(R.array.videoBackendValuesGL);
+ }
+ else if (deviceSupportsGLES3)
+ {
+ mVideoBackendPreference.setEntries(R.array.videoBackendEntriesGLES3);
+ mVideoBackendPreference.setEntryValues(R.array.videoBackendValuesGLES3);
+ }
+ else
+ {
+ mVideoBackendPreference.setEntries(R.array.videoBackendEntriesNoGLES3);
+ mVideoBackendPreference.setEntryValues(R.array.videoBackendValuesNoGLES3);
+ }
+
+ //
+ // Set available post processing shaders
+ //
+
+ List<CharSequence> shader_names = new ArrayList<CharSequence>();
+ List<CharSequence> shader_values = new ArrayList<CharSequence>();
+
+ // Disabled option
+ shader_names.add("Disabled");
+ shader_values.add("");
+
+ // TODO Since shaders are included with the APK, we know what they are at build-time. We should
+ // TODO be able to run this logic somehow at build-time and not rely on the device doing it.
+
+ File shaders_folder = new File(Environment.getExternalStorageDirectory() + File.separator + "dolphin-emu" + File.separator + "Shaders");
+ if (shaders_folder.exists())
+ {
+ File[] shaders = shaders_folder.listFiles();
+ for (File file : shaders)
+ {
+ if (file.isFile())
+ {
+ String filename = file.getName();
+ if (filename.endsWith(".glsl"))
+ {
+ // Strip the extension and put it in to the list
+ shader_names.add(filename.substring(0, filename.lastIndexOf('.')));
+ shader_values.add(filename.substring(0, filename.lastIndexOf('.')));
+ }
+ }
+ }
+ }
+
+ final ListPreference shader_preference = (ListPreference) findPreference("postProcessingShader");
+ shader_preference.setEntries(shader_names.toArray(new CharSequence[shader_names.size()]));
+ shader_preference.setEntryValues(shader_values.toArray(new CharSequence[shader_values.size()]));
+
+ //
+ // Disable all options if Software Rendering is used.
+ //
+ // Note that the numeric value in 'getPreference()'
+ // denotes the placement on the UI. So if more elements are
+ // added to the video settings, these may need to change.
+ //
+ mPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
+
+ if (mVideoBackendPreference.getValue().equals("Software Renderer"))
+ {
+ findPreference("enhancements").setEnabled(false);
+ findPreference("hacks").setEnabled(false);
+ findPreference("showFPS").setEnabled(false);
+ }
+ else if (mVideoBackendPreference.getValue().equals("OGL"))
+ {
+ findPreference("enhancements").setEnabled(true);
+ findPreference("hacks").setEnabled(true);
+ findPreference("showFPS").setEnabled(true);
+
+ // Check if we support stereo
+ // If we support desktop GL then we must support at least OpenGL 3.2
+ // If we only support OpenGLES then we need both OpenGLES 3.1 and AEP
+ if ((mEglHelper.supportsOpenGL() && mEglHelper.GetVersion() >= 320) ||
+ (mEglHelper.supportsGLES3() && mEglHelper.GetVersion() >= 310 && mEglHelper.SupportsExtension("GL_ANDROID_extension_pack_es31a")))
+ findPreference("StereoscopyScreen").setEnabled(true);
+ else
+ findPreference("StereoscopyScreen").setEnabled(false);
+ }
+
+ // Also set a listener, so that if someone changes the video backend, it will disable
+ // the video settings, upon the user choosing "Software Rendering".
+ mPreferences.registerOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences preferences, String key)
+ {
+ if (key.equals("gpuPref"))
+ {
+ if (preferences.getString(key, "Software Renderer").equals("Software Renderer"))
+ {
+ findPreference("enhancements").setEnabled(false);
+ findPreference("hacks").setEnabled(false);
+ findPreference("showFPS").setEnabled(false);
+ }
+ else if (preferences.getString(key, "Software Renderer").equals("OGL"))
+ {
+ findPreference("enhancements").setEnabled(true);
+ findPreference("hacks").setEnabled(true);
+ findPreference("showFPS").setEnabled(true);
+
+ // Create an alert telling them that their phone sucks
+ if (mEglHelper.supportsGLES3()
+ && mVendor.equals("Qualcomm")
+ && getQualcommVersion() == 14.0f)
+ {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(R.string.device_compat_warning);
+ builder.setMessage(R.string.device_gles3compat_warning_msg);
+ builder.setPositiveButton(R.string.yes, null);
+ builder.setNegativeButton(R.string.no, new DialogInterface.OnClickListener()
+ {
+ public void onClick(DialogInterface dialog, int which)
+ {
+ // Get an editor.
+ SharedPreferences.Editor editor = mPreferences.edit();
+ editor.putString("gpuPref", "Software Renderer");
+ editor.apply();
+ mVideoBackendPreference.setValue("Software Renderer");
+ }
+ });
+ builder.show();
+ }
+ }
+ }
+ }
+
+ private float getQualcommVersion()
+ {
+ final int start = mVersion.indexOf("V@") + 2;
+ final StringBuilder versionBuilder = new StringBuilder();
+
+ for (int i = start; i < mVersion.length(); i++)
+ {
+ char c = mVersion.charAt(i);
+
+ // End of numeric portion of version string.
+ if (c == ' ')
+ break;
+
+ versionBuilder.append(c);
+ }
+
+ return Float.parseFloat(versionBuilder.toString());
+ }
+}
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
new file mode 100644
index 0000000000..e6fdacc110
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/FileListItem.java
@@ -0,0 +1,95 @@
+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<FileListItem>
+{
+ 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<String> allowedExtensions = new HashSet<String>(Arrays.asList(".dff", ".dol", ".elf", ".gcm", ".gcz", ".iso", ".wad", ".wbfs"));
+
+ // Check that the file has an extension we care about before trying to read out of it.
+ if (allowedExtensions.contains(fileExtension))
+ {
+ // 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
new file mode 100644
index 0000000000..ae52261b45
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/Game.java
@@ -0,0 +1,154 @@
+package org.dolphinemu.dolphinemu.model;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+
+import java.io.File;
+
+public final class Game
+{
+ public static final int PLATFORM_GC = 0;
+ public static final int PLATFORM_WII = 1;
+ public static final int PLATFORM_WII_WARE = 2;
+
+ // 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:///sdcard/dolphin-emu/ScreenShots/";
+
+ private String mTitle;
+ private String mDescription;
+ private String mPath;
+ private String mGameId;
+ private String mScreenshotFolderPath;
+ private String mCompany;
+
+ private int mPlatform;
+ private int mCountry;
+
+ public Game(int platform, String title, String description, int country, String path, String gameId, String company)
+ {
+ mPlatform = platform;
+ mTitle = title;
+ mDescription = description;
+ mCountry = country;
+ mPath = path;
+ mGameId = gameId;
+ mCompany = company;
+ mScreenshotFolderPath = PATH_SCREENSHOT_FOLDER + getGameId() + "/";
+ }
+
+ public int 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 getScreenshotFolderPath()
+ {
+ return mScreenshotFolderPath;
+ }
+
+ public String getScreenPath()
+ {
+ // Count how many screenshots are available, so we can use the most recent one.
+ File screenshotFolder = new File(mScreenshotFolderPath.substring(mScreenshotFolderPath.indexOf('s') - 1));
+ int screenCount = 0;
+
+ if (screenshotFolder.isDirectory())
+ {
+ screenCount = screenshotFolder.list().length;
+ }
+
+ String screenPath = mScreenshotFolderPath
+ + getGameId() + "-"
+ + screenCount + ".png";
+
+ return screenPath;
+ }
+
+ public static ContentValues asContentValues(int platform, String title, String description, int country, String path, String gameId, String company)
+ {
+ 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_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(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));
+ }
+}
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
new file mode 100644
index 0000000000..6604717d92
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameDatabase.java
@@ -0,0 +1,240 @@
+package org.dolphinemu.dolphinemu.model;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
+
+import org.dolphinemu.dolphinemu.NativeLibrary;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 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 int FOLDER_COLUMN_PATH = 1;
+
+ 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 KEY_FOLDER_PATH = "path";
+
+ public static final String TABLE_NAME_FOLDERS = "folders";
+ 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 CONSTRAINT_UNIQUE = " UNIQUE";
+
+ 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_CREATE_FOLDERS = "CREATE TABLE " + TABLE_NAME_FOLDERS + "("
+ + KEY_DB_ID + TYPE_PRIMARY + SEPARATOR
+ + KEY_FOLDER_PATH + TYPE_STRING + CONSTRAINT_UNIQUE + ")";
+
+ private static final String SQL_DELETE_GAMES = "DROP TABLE IF EXISTS " + TABLE_NAME_GAMES;
+
+ public GameDatabase(Context context)
+ {
+ // Superclass constructor builds a database or uses an existing one.
+ super(context, "games.db", null, DB_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase database)
+ {
+ Log.d("DolphinEmu", "GameDatabase - Creating database...");
+
+ Log.v("DolphinEmu", "Executing SQL: " + SQL_CREATE_GAMES);
+ database.execSQL(SQL_CREATE_GAMES);
+
+ Log.v("DolphinEmu", "Executing SQL: " + SQL_CREATE_FOLDERS);
+ database.execSQL(SQL_CREATE_FOLDERS);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion)
+ {
+ Log.i("DolphinEmu", "Upgrading database from schema version " + oldVersion + " to " + newVersion);
+
+ Log.v("DolphinEmu", "Executing SQL: " + SQL_DELETE_GAMES);
+ database.execSQL(SQL_DELETE_GAMES);
+
+ Log.v("DolphinEmu", "Executing SQL: " + SQL_CREATE_GAMES);
+ database.execSQL(SQL_CREATE_GAMES);
+
+ Log.v("DolphinEmu", "Re-scanning library with new schema.");
+ scanLibrary(database);
+ }
+
+ 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.e("DolphinEmu", "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))});
+ }
+ }
+
+
+ // Get a cursor listing all the folders the user has added to the library.
+ Cursor folderCursor = database.query(TABLE_NAME_FOLDERS,
+ null, // Get all columns.
+ null, // Get all rows.
+ null,
+ null, // No grouping.
+ null,
+ null); // Order of folders is irrelevant.
+
+ Set<String> allowedExtensions = new HashSet<String>(Arrays.asList(".dff", ".dol", ".elf", ".gcm", ".gcz", ".iso", ".wad", ".wbfs"));
+
+ // Possibly overly defensive, but ensures that moveToNext() does not skip a row.
+ folderCursor.moveToPosition(-1);
+
+ // Iterate through all results of the DB query (i.e. all folders in the library.)
+ while (folderCursor.moveToNext())
+ {
+
+ String folderPath = folderCursor.getString(FOLDER_COLUMN_PATH);
+ File folder = new File(folderPath);
+
+ Log.i("DolphinEmu", "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))
+ {
+ 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);
+ }
+
+ ContentValues game = Game.asContentValues(NativeLibrary.GetPlatform(filePath),
+ name,
+ NativeLibrary.GetDescription(filePath).replace("\n", " "),
+ NativeLibrary.GetCountry(filePath),
+ filePath,
+ NativeLibrary.GetGameId(filePath),
+ 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.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));
+ }
+ }
+ }
+ }
+ }
+ }
+ // If the folder is empty because it no longer exists, remove it from the library.
+ else if (!folder.exists())
+ {
+ Log.e("DolphinEmu", "Folder no longer exists. Removing from the library: " + folderPath);
+ database.delete(TABLE_NAME_FOLDERS,
+ KEY_DB_ID + " = ?",
+ new String[]{Long.toString(folderCursor.getLong(COLUMN_DB_ID))});
+ }
+ else
+ {
+ Log.e("DolphinEmu", "Folder contains no games: " + folderPath);
+ }
+ }
+
+
+ folderCursor.close();
+ database.close();
+ }
+}
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
new file mode 100644
index 0000000000..dd83851a91
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameProvider.java
@@ -0,0 +1,149 @@
+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 REFRESH_LIBRARY = "refresh";
+
+ 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 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.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)
+ {
+ if (table.equals(REFRESH_LIBRARY))
+ {
+ Log.i("DolphinEmu", "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)
+ {
+ // 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;
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.java
new file mode 100644
index 0000000000..fc1a75cd0f
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.java
@@ -0,0 +1,290 @@
+/**
+ * Copyright 2013 Dolphin Emulator Project
+ * Licensed under GPLv2+
+ * Refer to the license.txt file included.
+ */
+
+package org.dolphinemu.dolphinemu.overlay;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.preference.PreferenceManager;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.View.OnTouchListener;
+
+import org.dolphinemu.dolphinemu.NativeLibrary;
+import org.dolphinemu.dolphinemu.NativeLibrary.ButtonState;
+import org.dolphinemu.dolphinemu.NativeLibrary.ButtonType;
+import org.dolphinemu.dolphinemu.R;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Draws the interactive input overlay on top of the
+ * {@link NativeGLSurfaceView} that is rendering emulation.
+ */
+public final class InputOverlay extends SurfaceView implements OnTouchListener
+{
+ private final Set<InputOverlayDrawableButton> overlayButtons = new HashSet<InputOverlayDrawableButton>();
+ private final Set<InputOverlayDrawableJoystick> overlayJoysticks = new HashSet<InputOverlayDrawableJoystick>();
+
+ /**
+ * Resizes a {@link Bitmap} by a given scale factor
+ *
+ * @param context The current {@link Context}
+ * @param bitmap The {@link Bitmap} to scale.
+ * @param scale The scale factor for the bitmap.
+ *
+ * @return The scaled {@link Bitmap}
+ */
+ public static Bitmap resizeBitmap(Context context, Bitmap bitmap, float scale)
+ {
+ // Retrieve screen dimensions.
+ DisplayMetrics dm = context.getResources().getDisplayMetrics();
+
+ Bitmap bitmapResized = Bitmap.createScaledBitmap(bitmap,
+ (int)(dm.heightPixels * scale),
+ (int)(dm.heightPixels * scale),
+ true);
+ return bitmapResized;
+ }
+
+ /**
+ * Constructor
+ *
+ * @param context The current {@link Context}.
+ * @param attrs {@link AttributeSet} for parsing XML attributes.
+ */
+ public InputOverlay(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+
+ // Add all the overlay items to the HashSet.
+ overlayButtons.add(initializeOverlayButton(context, R.drawable.gcpad_a, ButtonType.BUTTON_A));
+ overlayButtons.add(initializeOverlayButton(context, R.drawable.gcpad_b, ButtonType.BUTTON_B));
+ overlayButtons.add(initializeOverlayButton(context, R.drawable.gcpad_x, ButtonType.BUTTON_X));
+ overlayButtons.add(initializeOverlayButton(context, R.drawable.gcpad_y, ButtonType.BUTTON_Y));
+ overlayButtons.add(initializeOverlayButton(context, R.drawable.gcpad_z, ButtonType.BUTTON_Z));
+ overlayButtons.add(initializeOverlayButton(context, R.drawable.gcpad_start, ButtonType.BUTTON_START));
+ overlayButtons.add(initializeOverlayButton(context, R.drawable.gcpad_l, ButtonType.TRIGGER_L));
+ overlayButtons.add(initializeOverlayButton(context, R.drawable.gcpad_r, ButtonType.TRIGGER_R));
+ overlayJoysticks.add(initializeOverlayJoystick(context,
+ R.drawable.gcpad_joystick_range, R.drawable.gcpad_joystick,
+ ButtonType.STICK_MAIN));
+
+ // Set the on touch listener.
+ setOnTouchListener(this);
+
+ // Force draw
+ setWillNotDraw(false);
+
+ // Request focus for the overlay so it has priority on presses.
+ requestFocus();
+ }
+
+ @Override
+ public void draw(Canvas canvas)
+ {
+ super.draw(canvas);
+
+ for (InputOverlayDrawableButton button : overlayButtons)
+ {
+ button.draw(canvas);
+ }
+
+ for (InputOverlayDrawableJoystick joystick: overlayJoysticks)
+ {
+ joystick.draw(canvas);
+ }
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event)
+ {
+ int pointerIndex = event.getActionIndex();
+
+ for (InputOverlayDrawableButton button : overlayButtons)
+ {
+ // Determine the button state to apply based on the MotionEvent action flag.
+ switch(event.getAction() & MotionEvent.ACTION_MASK)
+ {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_POINTER_DOWN:
+ case MotionEvent.ACTION_MOVE:
+ // If a pointer enters the bounds of a button, press that button.
+ if (button.getBounds().contains((int)event.getX(pointerIndex), (int)event.getY(pointerIndex)))
+ NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, button.getId(), ButtonState.PRESSED);
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_POINTER_UP:
+ // If a pointer ends, release the button it was pressing.
+ if (button.getBounds().contains((int)event.getX(pointerIndex), (int)event.getY(pointerIndex)))
+ NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, button.getId(), ButtonState.RELEASED);
+ break;
+ }
+ }
+
+
+ for (InputOverlayDrawableJoystick joystick : overlayJoysticks)
+ {
+ joystick.TrackEvent(event);
+ int[] axisIDs = joystick.getAxisIDs();
+ float[] axises = joystick.getAxisValues();
+
+ for (int i = 0; i < 4; i++)
+ NativeLibrary.onGamePadMoveEvent(NativeLibrary.TouchScreenDevice, axisIDs[i], axises[i]);
+ }
+
+ return true;
+ }
+
+ /**
+ * Initializes an InputOverlayDrawableButton, given by resId, with all of the
+ * parameters set for it to be properly shown on the InputOverlay.
+ * <p>
+ * This works due to the way the X and Y coordinates are stored within
+ * the {@link SharedPreferences}.
+ * <p>
+ * In the input overlay configuration menu,
+ * once a touch event begins and then ends (ie. Organizing the buttons to one's own liking for the overlay).
+ * the X and Y coordinates of the button at the END of its touch event
+ * (when you remove your finger/stylus from the touchscreen) are then stored
+ * within a SharedPreferences instance so that those values can be retrieved here.
+ * <p>
+ * This has a few benefits over the conventional way of storing the values
+ * (ie. within the Dolphin ini file).
+ * <ul>
+ * <li>No native calls</li>
+ * <li>Keeps Android-only values inside the Android environment</li>
+ * </ul>
+ * <p>
+ * Technically no modifications should need to be performed on the returned
+ * InputOverlayDrawableButton. Simply add it to the HashSet of overlay items and wait
+ * for Android to call the onDraw method.
+ *
+ * @param context The current {@link Context}.
+ * @param resId The resource ID of the {@link Drawable} to get the {@link Bitmap} of.
+ * @param buttonId Identifier for determining what type of button the initialized InputOverlayDrawableButton represents.
+ *
+ * @return An {@link InputOverlayDrawableButton} with the correct drawing bounds set.
+ *
+ */
+ private static InputOverlayDrawableButton initializeOverlayButton(Context context, int resId, int buttonId)
+ {
+ // Resources handle for fetching the initial Drawable resource.
+ final Resources res = context.getResources();
+
+ // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableButton.
+ final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context);
+
+ // Decide scale based on button ID
+ float scale;
+ float overlaySize = sPrefs.getInt("controls_size", 25);
+ overlaySize += 25;
+ overlaySize /= 50;
+
+ switch (resId)
+ {
+ case R.drawable.gcpad_b:
+ scale = 0.13f * overlaySize;
+ break;
+ case R.drawable.gcpad_x:
+ case R.drawable.gcpad_y:
+ scale = 0.18f * overlaySize;
+ break;
+ case R.drawable.gcpad_start:
+ scale = 0.12f * overlaySize;
+ break;
+ default:
+ scale = 0.20f * overlaySize;
+ break;
+ }
+
+ // Initialize the InputOverlayDrawableButton.
+ final Bitmap bitmap = resizeBitmap(context, BitmapFactory.decodeResource(res, resId), scale);
+ final InputOverlayDrawableButton overlayDrawable = new InputOverlayDrawableButton(res, bitmap, buttonId);
+
+ // String ID of the Drawable. This is what is passed into SharedPreferences
+ // to check whether or not a value has been set.
+ final String drawableId = res.getResourceEntryName(resId);
+
+ // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
+ // These were set in the input overlay configuration menu.
+ int drawableX = (int) sPrefs.getFloat(drawableId+"-X", 0f);
+ int drawableY = (int) sPrefs.getFloat(drawableId+"-Y", 0f);
+
+ // Intrinsic width and height of the InputOverlayDrawableButton.
+ // For any who may not know, intrinsic width/height
+ // are the original unmodified width and height of the image.
+ int intrinWidth = overlayDrawable.getIntrinsicWidth();
+ int intrinHeight = overlayDrawable.getIntrinsicHeight();
+
+ // Now set the bounds for the InputOverlayDrawableButton.
+ // This will dictate where on the screen (and the what the size) the InputOverlayDrawableButton will be.
+ overlayDrawable.setBounds(drawableX, drawableY, drawableX+intrinWidth, drawableY+intrinHeight);
+
+ return overlayDrawable;
+ }
+
+ /**
+ * Initializes an {@link InputOverlayDrawableJoystick}
+ *
+ * @param context The current {@link Context}
+ * @param resOuter Resource ID for the outer image of the joystick (the static image that shows the circular bounds).
+ * @param resInner Resource ID for the inner image of the joystick (the one you actually move around).
+ * @param joystick Identifier for which joystick this is.
+ *
+ * @return the initialized {@link InputOverlayDrawableJoystick}.
+ */
+ private static InputOverlayDrawableJoystick initializeOverlayJoystick(Context context, int resOuter, int resInner, int joystick)
+ {
+ // Resources handle for fetching the initial Drawable resource.
+ final Resources res = context.getResources();
+
+ // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableJoystick.
+ final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context);
+
+ // Initialize the InputOverlayDrawableJoystick.
+ float overlaySize = sPrefs.getInt("controls_size", 20);
+ overlaySize += 30;
+ overlaySize /= 50;
+ final Bitmap bitmapOuter = resizeBitmap(context, BitmapFactory.decodeResource(res, resOuter), 0.30f * overlaySize);
+ final Bitmap bitmapInner = BitmapFactory.decodeResource(res, resInner);
+
+ // String ID of the Drawable. This is what is passed into SharedPreferences
+ // to check whether or not a value has been set.
+ final String drawableId = res.getResourceEntryName(resOuter);
+
+ // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
+ // These were set in the input overlay configuration menu.
+ int drawableX = (int) sPrefs.getFloat(drawableId+"-X", 0f);
+ int drawableY = (int) sPrefs.getFloat(drawableId+"-Y", 0f);
+
+ // Now set the bounds for the InputOverlayDrawableJoystick.
+ // This will dictate where on the screen (and the what the size) the InputOverlayDrawableJoystick will be.
+ int outerSize = bitmapOuter.getWidth();
+ Rect outerRect = new Rect(drawableX, drawableY, drawableX + outerSize, drawableY + outerSize);
+ Rect innerRect = new Rect(0, 0, outerSize / 4, outerSize / 4);
+
+ final InputOverlayDrawableJoystick overlayDrawable
+ = new InputOverlayDrawableJoystick(res,
+ bitmapOuter, bitmapInner,
+ outerRect, innerRect,
+ joystick);
+
+
+ return overlayDrawable;
+ }
+
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableButton.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableButton.java
new file mode 100644
index 0000000000..e02e352e56
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableButton.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright 2013 Dolphin Emulator Project
+ * Licensed under GPLv2+
+ * Refer to the license.txt file included.
+ */
+
+package org.dolphinemu.dolphinemu.overlay;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+
+/**
+ * Custom {@link BitmapDrawable} that is capable
+ * of storing it's own ID.
+ */
+public final class InputOverlayDrawableButton extends BitmapDrawable
+{
+ // The ID identifying what type of button this Drawable represents.
+ private int buttonType;
+
+ /**
+ * Constructor
+ *
+ * @param res {@link Resources} instance.
+ * @param bitmap {@link Bitmap} to use with this Drawable.
+ * @param buttonType Identifier for this type of button.
+ */
+ public InputOverlayDrawableButton(Resources res, Bitmap bitmap, int buttonType)
+ {
+ super(res, bitmap);
+
+ this.buttonType = buttonType;
+ }
+
+ /**
+ * Gets this InputOverlayDrawableButton's button ID.
+ *
+ * @return this InputOverlayDrawableButton's button ID.
+ */
+ public int getId()
+ {
+ return buttonType;
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableJoystick.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableJoystick.java
new file mode 100644
index 0000000000..d4082e4289
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableJoystick.java
@@ -0,0 +1,137 @@
+/**
+ * Copyright 2013 Dolphin Emulator Project
+ * Licensed under GPLv2+
+ * Refer to the license.txt file included.
+ */
+
+package org.dolphinemu.dolphinemu.overlay;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.view.MotionEvent;
+
+/**
+ * Custom {@link BitmapDrawable} that is capable
+ * of storing it's own ID.
+ */
+public final class InputOverlayDrawableJoystick extends BitmapDrawable
+{
+ private final int[] axisIDs = {0, 0, 0, 0};
+ private final float[] axises = {0f, 0f};
+ private final BitmapDrawable ringInner;
+ private int trackId = -1;
+
+ /**
+ * Constructor
+ *
+ * @param res {@link Resources} instance.
+ * @param bitmapOuter {@link Bitmap} which represents the outer non-movable part of the joystick.
+ * @param bitmapInner {@link Bitmap} which represents the inner movable part of the joystick.
+ * @param rectOuter {@link Rect} which represents the outer joystick bounds.
+ * @param rectInner {@link Rect} which represents the inner joystick bounds.
+ * @param joystick Identifier for which joystick this is.
+ */
+ public InputOverlayDrawableJoystick(Resources res,
+ Bitmap bitmapOuter, Bitmap bitmapInner,
+ Rect rectOuter, Rect rectInner,
+ int joystick)
+ {
+ super(res, bitmapOuter);
+ this.setBounds(rectOuter);
+
+ this.ringInner = new BitmapDrawable(res, bitmapInner);
+ this.ringInner.setBounds(rectInner);
+ SetInnerBounds();
+ this.axisIDs[0] = joystick + 1;
+ this.axisIDs[1] = joystick + 2;
+ this.axisIDs[2] = joystick + 3;
+ this.axisIDs[3] = joystick + 4;
+ }
+
+ @Override
+ public void draw(Canvas canvas)
+ {
+ super.draw(canvas);
+
+ ringInner.draw(canvas);
+ }
+
+ public void TrackEvent(MotionEvent event)
+ {
+ int pointerIndex = event.getActionIndex();
+
+ switch(event.getAction() & MotionEvent.ACTION_MASK)
+ {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_POINTER_DOWN:
+ if (getBounds().contains((int)event.getX(pointerIndex), (int)event.getY(pointerIndex)))
+ trackId = event.getPointerId(pointerIndex);
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_POINTER_UP:
+ if (trackId == event.getPointerId(pointerIndex))
+ {
+ axises[0] = axises[1] = 0.0f;
+ SetInnerBounds();
+ trackId = -1;
+ }
+ break;
+ }
+
+ if (trackId == -1)
+ return;
+
+ for (int i = 0; i < event.getPointerCount(); i++)
+ {
+ if (trackId == event.getPointerId(i))
+ {
+ float touchX = event.getX(i);
+ float touchY = event.getY(i);
+ float maxY = getBounds().bottom;
+ float maxX = getBounds().right;
+ touchX -= getBounds().centerX();
+ maxX -= getBounds().centerX();
+ touchY -= getBounds().centerY();
+ maxY -= getBounds().centerY();
+ final float AxisX = touchX / maxX;
+ final float AxisY = touchY / maxY;
+ axises[0] = AxisY;
+ axises[1] = AxisX;
+
+ SetInnerBounds();
+ }
+ }
+ }
+
+ public float[] getAxisValues()
+ {
+ float[] joyaxises = {0f, 0f, 0f, 0f};
+ joyaxises[1] = Math.min(axises[0], 1.0f);
+ joyaxises[0] = Math.min(axises[0], 0.0f);
+ joyaxises[3] = Math.min(axises[1], 1.0f);
+ joyaxises[2] = Math.min(axises[1], 0.0f);
+ return joyaxises;
+ }
+
+ public int[] getAxisIDs()
+ {
+ return axisIDs;
+ }
+
+ private void SetInnerBounds()
+ {
+ float floatX = this.getBounds().centerX();
+ float floatY = this.getBounds().centerY();
+ floatY += axises[0] * (this.getBounds().height() / 2);
+ floatX += axises[1] * (this.getBounds().width() / 2);
+ int X = (int)(floatX);
+ int Y = (int)(floatY);
+ int width = this.ringInner.getBounds().width() / 2;
+ int height = this.ringInner.getBounds().height() / 2;
+ this.ringInner.setBounds(X - width, Y - height,
+ X + width, Y + height);
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/AssetCopyService.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/AssetCopyService.java
new file mode 100644
index 0000000000..1f6f94bc8a
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/AssetCopyService.java
@@ -0,0 +1,124 @@
+/**
+ * Copyright 2014 Dolphin Emulator Project
+ * Licensed under GPLv2+
+ * Refer to the license.txt file included.
+ */
+
+package org.dolphinemu.dolphinemu.services;
+
+import android.app.IntentService;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+import org.dolphinemu.dolphinemu.NativeLibrary;
+import org.dolphinemu.dolphinemu.utils.UserPreferences;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * A service that spawns its own thread in order to copy several binary and shader files
+ * from the Dolphin APK to the external file system.
+ */
+public final class AssetCopyService extends IntentService
+{
+ private static final String TAG = "DolphinEmulator";
+
+ public AssetCopyService()
+ {
+ // Superclass constructor is called to name the thread on which this service executes.
+ super("AssetCopyService");
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent)
+ {
+ String BaseDir = NativeLibrary.GetUserDirectory();
+ String ConfigDir = BaseDir + File.separator + "Config";
+ String GCDir = BaseDir + File.separator + "GC";
+
+ // Copy assets if needed
+ File file = new File(GCDir + File.separator + "font_sjis.bin");
+ if(!file.exists())
+ {
+ NativeLibrary.CreateUserFolders();
+ copyAsset("dsp_coef.bin", GCDir + File.separator + "dsp_coef.bin");
+ copyAsset("dsp_rom.bin", GCDir + File.separator + "dsp_rom.bin");
+ copyAsset("font_ansi.bin", GCDir + File.separator + "font_ansi.bin");
+ copyAsset("font_sjis.bin", GCDir + File.separator + "font_sjis.bin");
+ copyAssetFolder("Shaders", BaseDir + File.separator + "Shaders");
+ }
+ else
+ {
+ Log.v(TAG, "Skipping asset copy operation.");
+ }
+
+ // Always copy over the GCPad config in case of change or corruption.
+ // Not a user configurable file.
+ copyAsset("GCPadNew.ini", ConfigDir + File.separator + "GCPadNew.ini");
+
+ // Load the configuration keys set in the Dolphin ini and gfx ini files
+ // into the application's shared preferences.
+ UserPreferences.LoadIniToPrefs(this);
+
+ // Record the fact that we've done this before, so we don't do it on every launch.
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
+ SharedPreferences.Editor editor = preferences.edit();
+
+ editor.putBoolean("assetsCopied", true);
+ editor.commit();
+ }
+
+ private void copyAsset(String asset, String output)
+ {
+ Log.v(TAG, "Copying " + asset + " to " + output);
+ InputStream in = null;
+ OutputStream out = null;
+
+ try
+ {
+ in = getAssets().open(asset);
+ out = new FileOutputStream(output);
+ copyFile(in, out);
+ in.close();
+ out.close();
+ }
+ catch (IOException e)
+ {
+ Log.e(TAG, "Failed to copy asset file: " + asset, e);
+ }
+ }
+
+ private void copyAssetFolder(String assetFolder, String outputFolder)
+ {
+ Log.v(TAG, "Copying " + assetFolder + " to " + outputFolder);
+
+ try
+ {
+ for (String file : getAssets().list(assetFolder))
+ {
+ copyAsset(assetFolder + File.separator + file, outputFolder + File.separator + file);
+ }
+ }
+ catch (IOException e)
+ {
+ Log.e(TAG, "Failed to copy asset folder: " + assetFolder, e);
+ }
+ }
+
+ private void copyFile(InputStream in, OutputStream out) throws IOException
+ {
+ byte[] buffer = new byte[1024];
+ int read;
+
+ while ((read = in.read(buffer)) != -1)
+ {
+ out.write(buffer, 0, read);
+ }
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/SettingsSaveService.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/SettingsSaveService.java
new file mode 100644
index 0000000000..1cf018ed16
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/SettingsSaveService.java
@@ -0,0 +1,30 @@
+package org.dolphinemu.dolphinemu.services;
+
+import android.app.IntentService;
+import android.content.Intent;
+import android.util.Log;
+
+import org.dolphinemu.dolphinemu.utils.UserPreferences;
+
+/**
+ * IntentServices, unlike regular services, inherently run on a background thread.
+ * This IntentService saves all the options the user set in the Java-based UI into
+ * INI files the native code can read.
+ */
+public final class SettingsSaveService extends IntentService
+{
+ private static final String TAG = "DolphinEmulator";
+
+ public SettingsSaveService()
+ {
+ super("SettingsSaveService");
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent)
+ {
+ Log.v(TAG, "Saving settings to INI files...");
+ UserPreferences.SavePrefsToIni(this);
+ Log.v(TAG, "Save successful.");
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CPUHelper.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CPUHelper.java
new file mode 100644
index 0000000000..30be6e2213
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CPUHelper.java
@@ -0,0 +1,395 @@
+package org.dolphinemu.dolphinemu.utils;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+
+import org.dolphinemu.dolphinemu.R;
+
+import android.content.Context;
+import android.os.Build;
+import android.util.Log;
+
+/**
+ * Utility class for retrieving information
+ * from a device's CPU.
+ */
+public final class CPUHelper
+{
+ private int revision;
+ private int variant;
+ private int numCores;
+ private String implementerID = "N/A";
+ private String part = "N/A";
+ private String hardware = "N/A";
+ private String processorInfo = "N/A";
+ private String features = "N/A";
+
+ private final Context ctx;
+
+ /**
+ * Constructor
+ *
+ * @param ctx The current {@link Context}.
+ */
+ public CPUHelper(Context ctx)
+ {
+ this.ctx = ctx;
+
+ try
+ {
+ // TODO: Should do other architectures as well (x86 and MIPS).
+ // Can do differentiating between platforms by using
+ // android.os.Build.CPU_ABI.
+ //
+ // CPU_ABI.contains("armeabi") == get ARM info.
+ // CPU_ABI.contains("x86") == get x86 info.
+ // CPU_ABI.contains("mips") == get MIPS info.
+ //
+ // However additional testing should be done across devices,
+ // I highly doubt /proc/cpuinfo retains the same formatting
+ // on different architectures.
+ //
+ // If push comes to shove, we can simply spit out the cpuinfo
+ // contents. I would like to avoid this if possible, however.
+
+ if (Build.CPU_ABI.contains("arm"))
+ {
+ getARMInfo();
+ }
+ else
+ {
+ Log.e("CPUHelper", "CPU architecture not supported yet.");
+ }
+ }
+ catch (IOException ioe)
+ {
+ Log.e("CPUHelper", ioe.getMessage());
+ }
+ }
+
+ /**
+ * Gets the revision number of the CPU.
+ *
+ * @return the revision number of the CPU.
+ */
+ public int getRevision()
+ {
+ return revision;
+ }
+
+
+ /**
+ * Gets the CPU variant number.
+ *
+ * @return the CPU variant number.
+ */
+ public int getVariant()
+ {
+ return variant;
+ }
+
+ /**
+ * Gets the total number of cores in the CPU.
+ *
+ * @return the total number of cores in the CPU.
+ */
+ public int getNumCores()
+ {
+ return numCores;
+ }
+
+ /**
+ * Gets the name of the implementer of the CPU.
+ *
+ * @return the name of the implementer of the CPU.
+ */
+ public String getImplementer()
+ {
+ return implementerID;
+ }
+
+ /**
+ * Gets the specific processor type of the CPU.
+ *
+ * @return the specific processor type.
+ */
+ public String getProcessorType()
+ {
+ return part;
+ }
+
+ /**
+ * Gets the internal name of the hardware.
+ *
+ * @return the internal name of the hardware.
+ */
+ public String getHardware()
+ {
+ return hardware;
+ }
+
+ /**
+ * Get the processor info string.
+ *
+ * @return the processor info string.
+ */
+ public String getProcessorInfo()
+ {
+ return processorInfo;
+ }
+
+ /**
+ * Gets the features supported by the CPU.
+ *
+ * @return the features supported by the CPU.
+ */
+ public String getFeatures()
+ {
+ return features;
+ }
+
+ /**
+ * Whether or not this CPU is using the ARM architecture.
+ *
+ * @return true if this CPU uses the ARM architecture; false otherwise.
+ */
+ public static boolean isARM()
+ {
+ return Build.CPU_ABI.contains("arm");
+ }
+
+ /**
+ * Whether or not this CPU is using the ARM64 architecture.
+ *
+ * @return true if this CPU uses the ARM64 architecture; false otherwise.
+ */
+ public static boolean isARM64() { return Build.CPU_ABI.contains("arm64"); }
+
+ /**
+ * Whether or not this CPU is using the x86 architecture.
+ *
+ * @return true if this CPU uses the x86 architecture; false otherwise.
+ */
+ public static boolean isX86()
+ {
+ return Build.CPU_ABI.contains("x86");
+ }
+
+ /**
+ * Whether or not this CPU is using the MIPS architecture.
+ *
+ * @return true if this CPU uses the MIPS architecture; false otherwise.
+ */
+ public static boolean isMIPS()
+ {
+ return Build.CPU_ABI.contains("mips");
+ }
+
+ // Retrieves information for ARM CPUs.
+ private void getARMInfo() throws IOException
+ {
+ File info = new File("/proc/cpuinfo");
+ if (info.exists())
+ {
+ BufferedReader br = new BufferedReader(new FileReader(info));
+
+ String line;
+ while ((line = br.readLine()) != null)
+ {
+ if (line.contains("Processor\t:"))
+ {
+ this.processorInfo = parseLine(line);
+ }
+ else if (line.contains("Hardware\t:"))
+ {
+ this.hardware = parseLine(line);
+ }
+ else if (line.contains("Features\t:"))
+ {
+ this.features = parseLine(line);
+ }
+ else if (line.contains("CPU implementer\t:"))
+ {
+ this.implementerID = parseArmID(Integer.decode(parseLine(line)));
+ }
+ else if (line.contains("CPU part\t:"))
+ {
+ this.part = parseArmPartNumber(Integer.decode(parseLine(line)));
+ }
+ else if (line.contains("CPU revision\t:"))
+ {
+ this.revision = Integer.decode(parseLine(line));
+ }
+ else if (line.contains("CPU variant\t:"))
+ {
+ this.variant = Integer.decode(parseLine(line));
+ }
+ else if (line.contains("processor\t:")) // Lower case indicates a specific core number
+ {
+ this.numCores++;
+ }
+ }
+
+ br.close();
+ }
+ }
+
+ // Basic function for parsing cpuinfo format strings.
+ // cpuinfo format strings consist of [label:info] parts.
+ // We only want to retrieve the info portion so we split
+ // them using ':' as a delimeter.
+ private String parseLine(String line)
+ {
+ String[] temp = line.split(":");
+ if (temp.length != 2)
+ return "N/A";
+
+ return temp[1].trim();
+ }
+
+ // Parses an ARM CPU ID.
+ private String parseArmID(int id)
+ {
+ switch (id)
+ {
+ case 0x41:
+ return "ARM Limited";
+
+ case 0x44:
+ return "Digital Equipment Corporation";
+
+ case 0x4D:
+ return "Freescale Semiconductor Inc.";
+
+ case 0x4E:
+ return "Nvidia Corporation";
+
+ case 0x51:
+ return "Qualcomm Inc.";
+
+ case 0x56:
+ return "Marvell Semiconductor Inc.";
+
+ case 0x69:
+ return "Intel Corporation";
+
+ default:
+ return "N/A";
+ }
+ }
+
+ // Parses the ARM CPU Part number.
+ private String parseArmPartNumber(int partNum)
+ {
+ switch (partNum)
+ {
+ // Qualcomm part numbers.
+ case 0x00F:
+ return "Qualcomm Scorpion";
+
+ case 0x02D:
+ return "Qualcomm Dual Scorpion";
+
+ case 0x04D:
+ return "Qualcomm Dual Krait";
+
+ case 0x06F:
+ return "Qualcomm Quad Krait";
+
+ // Marvell Semiconductor part numbers
+ case 0x131:
+ return "Marvell Feroceon";
+
+ case 0x581:
+ return "Marvell PJ4/PJ4b";
+
+ case 0x584:
+ return "Marvell Dual PJ4/PJ4b";
+
+ // Official ARM part numbers.
+ case 0x920:
+ return "ARM920";
+
+ case 0x922:
+ return "ARM922";
+
+ case 0x926:
+ return "ARM926";
+
+ case 0x940:
+ return "ARM940";
+
+ case 0x946:
+ return "ARM946";
+
+ case 0x966:
+ return "ARM966";
+
+ case 0x968:
+ return "ARM968";
+
+ case 0xB02:
+ return "ARM11 MPCore";
+
+ case 0xB36:
+ return "ARM1136";
+
+ case 0xB56:
+ return "ARM1156";
+
+ case 0xB76:
+ return "ARM1176";
+
+ case 0xC05:
+ return "ARM Cortex A5";
+
+ case 0xC07:
+ return "ARM Cortex-A7 MPCore";
+
+ case 0xC08:
+ return "ARM Cortex A8";
+
+ case 0xC09:
+ return "ARM Cortex A9";
+
+ case 0xC0C:
+ return "ARM Cortex A12";
+
+ case 0xC0F:
+ return "ARM Cortex A15";
+
+ case 0xC14:
+ return "ARM Cortex R4";
+
+ case 0xC15:
+ return "ARM Cortex R5";
+
+ case 0xC20:
+ return "ARM Cortex M0";
+
+ case 0xC21:
+ return "ARM Cortex M1";
+
+ case 0xC23:
+ return "ARM Cortex M3";
+
+ case 0xC24:
+ return "ARM Cortex M4";
+
+ case 0xC60:
+ return "ARM Cortex M0+";
+
+ case 0xD03:
+ return "ARM Cortex A53";
+
+ case 0xD07:
+ return "ARM Cortex A57 MPCore";
+
+
+ default: // Unknown/Not yet added to list.
+ return String.format(ctx.getString(R.string.unknown_part_num), partNum);
+ }
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/EGLHelper.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/EGLHelper.java
new file mode 100644
index 0000000000..24d192d412
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/EGLHelper.java
@@ -0,0 +1,393 @@
+/**
+ * Copyright 2013 Dolphin Emulator Project
+ * Licensed under GPLv2+
+ * Refer to the license.txt file included.
+ */
+
+package org.dolphinemu.dolphinemu.utils;
+
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.egl.EGLContext;
+import javax.microedition.khronos.egl.EGLDisplay;
+import javax.microedition.khronos.egl.EGLSurface;
+import javax.microedition.khronos.opengles.GL10;
+
+import android.opengl.GLES30;
+import android.util.Log;
+
+import org.dolphinemu.dolphinemu.NativeLibrary;
+
+/**
+ * Utility class that abstracts all the stuff about
+ * EGL initialization out of the way if all that is
+ * wanted is to query the underlying GL API for information.
+ */
+public final class EGLHelper
+{
+ private final EGL10 mEGL;
+ private final EGLDisplay mDisplay;
+ private EGLConfig[] mEGLConfigs;
+ private EGLContext mEGLContext;
+ private EGLSurface mEGLSurface;
+ private GL10 mGL;
+
+ // GL support flags
+ private boolean supportGL;
+ private boolean supportGLES2;
+ private boolean supportGLES3;
+
+ // Renderable type bitmasks
+ public static final int EGL_OPENGL_ES_BIT = 0x0001;
+ public static final int EGL_OPENGL_ES2_BIT = 0x0004;
+ public static final int EGL_OPENGL_BIT = 0x0008;
+ public static final int EGL_OPENGL_ES3_BIT_KHR = 0x0040;
+
+ // API types
+ public static final int EGL_OPENGL_ES_API = 0x30A0;
+ public static final int EGL_OPENGL_API = 0x30A2;
+
+ /**
+ * Constructor
+ * <p>
+ * Initializes the underlying {@link EGLSurface} with a width and height of 1.
+ * This is useful if all you need to use this class for is to query information
+ * from specific API contexts.
+ *
+ * @param renderableType Bitmask indicating which types of client API contexts
+ * the framebuffer config must support.
+ */
+ public EGLHelper(int renderableType)
+ {
+ this(1, 1, renderableType);
+ }
+
+ /**
+ * Constructor
+ *
+ * @param width Width of the underlying {@link EGLSurface}.
+ * @param height Height of the underlying {@link EGLSurface}.
+ * @param renderableType Bitmask indicating which types of client API contexts
+ * the framebuffer config must support.
+ */
+ public EGLHelper(int width, int height, int renderableType)
+ {
+ // Initialize handle to an EGL display.
+ mEGL = (EGL10) EGLContext.getEGL();
+ mDisplay = mEGL.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
+
+ // If a display is present, initialize EGL.
+ if (mDisplay != EGL10.EGL_NO_DISPLAY)
+ {
+ int[] version = new int[2];
+ if (mEGL.eglInitialize(mDisplay, version))
+ {
+ // Detect supported GL APIs, initialize configs, etc.
+ detect();
+
+ // Create context and surface
+ create(width, height, renderableType);
+ }
+ else
+ {
+ Log.e("EGLHelper", "Error initializing EGL.");
+ }
+ }
+ else
+ {
+ Log.e("EGLHelper", "Error initializing EGL display.");
+ }
+ }
+
+ /**
+ * Releases all resources associated with this helper.
+ * <p>
+ * This should be called whenever this helper is no longer needed.
+ */
+ public void closeHelper()
+ {
+ mEGL.eglTerminate(mDisplay);
+ }
+
+ /**
+ * Gets information through EGL.<br/>
+ * <p>
+ * Index 0: Vendor <br/>
+ * Index 1: Version <br/>
+ * Index 2: Renderer <br/>
+ * Index 3: Extensions <br/>
+ *
+ * @return information retrieved through EGL.
+ */
+ public String[] getEGLInfo()
+ {
+ String[] info = {
+ mGL.glGetString(GL10.GL_VENDOR),
+ mGL.glGetString(GL10.GL_VERSION),
+ mGL.glGetString(GL10.GL_RENDERER),
+ mGL.glGetString(GL10.GL_EXTENSIONS),
+ };
+
+ return info;
+ }
+
+ /**
+ * Whether or not this device supports OpenGL.
+ *
+ * @return true if this device supports OpenGL; false otherwise.
+ */
+ public boolean supportsOpenGL()
+ {
+ return supportGL;
+ }
+
+ /**
+ * Whether or not this device supports OpenGL ES 2.
+ * <br/>
+ * Note that if this returns true, then OpenGL ES 1 is also supported.
+ *
+ * @return true if this device supports OpenGL ES 2; false otherwise.
+ */
+ public boolean supportsGLES2()
+ {
+ return supportGLES2;
+ }
+
+ /**
+ * Whether or not this device supports OpenGL ES 3.
+ * <br/>
+ * Note that if this returns true, then OpenGL ES 1 and 2 are also supported.
+ *
+ * @return true if this device supports OpenGL ES 3; false otherwise.
+ */
+ public boolean supportsGLES3()
+ {
+ return supportGLES3;
+ }
+
+ /**
+ * Gets the underlying {@link EGL10} instance.
+ *
+ * @return the underlying {@link EGL10} instance.
+ */
+ public EGL10 getEGL()
+ {
+ return mEGL;
+ }
+
+ /**
+ * Gets the underlying {@link GL10} instance.
+ *
+ * @return the underlying {@link GL10} instance.
+ */
+ public GL10 getGL()
+ {
+ return mGL;
+ }
+
+ /**
+ * Gets the underlying {@link EGLDisplay}.
+ *
+ * @return the underlying {@link EGLDisplay}
+ */
+ public EGLDisplay getDisplay()
+ {
+ return mDisplay;
+ }
+
+ /**
+ * Gets all supported framebuffer configurations for this device.
+ *
+ * @return all supported framebuffer configurations for this device.
+ */
+ public EGLConfig[] getConfigs()
+ {
+ return mEGLConfigs;
+ }
+
+ /**
+ * Gets the underlying {@link EGLContext}.
+ *
+ * @return the underlying {@link EGLContext}.
+ */
+ public EGLContext getContext()
+ {
+ return mEGLContext;
+ }
+
+ /**
+ * Gets the underlying {@link EGLSurface}.
+ *
+ * @return the underlying {@link EGLSurface}.
+ */
+ public EGLSurface getSurface()
+ {
+ return mEGLSurface;
+ }
+
+ // Detects the specific kind of GL modes that are supported
+ private boolean detect()
+ {
+ // Get total number of configs available.
+ int[] numConfigs = new int[1];
+ if (!mEGL.eglGetConfigs(mDisplay, null, 0, numConfigs))
+ {
+ Log.e("EGLHelper", "Error retrieving number of EGL configs available.");
+ return false;
+ }
+
+ // Now get all the configurations
+ mEGLConfigs = new EGLConfig[numConfigs[0]];
+ if (!mEGL.eglGetConfigs(mDisplay, mEGLConfigs, mEGLConfigs.length, numConfigs))
+ {
+ Log.e("EGLHelper", "Error retrieving all EGL configs.");
+ return false;
+ }
+
+ for (int i = 0; i < mEGLConfigs.length; i++)
+ {
+ int[] attribVal = new int[1];
+ boolean ret = mEGL.eglGetConfigAttrib(mDisplay, mEGLConfigs[i], EGL10.EGL_RENDERABLE_TYPE, attribVal);
+ if (ret)
+ {
+ if ((attribVal[0] & EGL_OPENGL_BIT) != 0)
+ supportGL = true;
+
+ if ((attribVal[0] & EGL_OPENGL_ES2_BIT) != 0)
+ supportGLES2 = true;
+
+ if ((attribVal[0] & EGL_OPENGL_ES3_BIT_KHR) != 0)
+ supportGLES3 = true;
+ }
+ }
+
+ return true;
+ }
+
+ // Creates the context and surface.
+ private void create(int width, int height, int renderableType)
+ {
+ int[] attribs = {
+ EGL10.EGL_WIDTH, width,
+ EGL10.EGL_HEIGHT, height,
+ EGL10.EGL_NONE
+ };
+
+ // Initially we just assume GLES2 will be the default context.
+ int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
+ int[] ctx_attribs = {
+ EGL_CONTEXT_CLIENT_VERSION, 2,
+ EGL10.EGL_NONE
+ };
+
+ // Determine the type of context that will be created
+ // and change the attribute arrays accordingly.
+ switch (renderableType)
+ {
+ case EGL_OPENGL_ES_BIT:
+ ctx_attribs[1] = 1;
+ break;
+
+ case EGL_OPENGL_BIT:
+ ctx_attribs[0] = EGL10.EGL_NONE;
+ break;
+
+ case EGL_OPENGL_ES3_BIT_KHR:
+ ctx_attribs[1] = 3;
+ break;
+
+ case EGL_OPENGL_ES2_BIT:
+ default: // Fall-back to GLES 2.
+ ctx_attribs[1] = 2;
+ break;
+ }
+ if (renderableType == EGL_OPENGL_BIT)
+ NativeLibrary.eglBindAPI(EGL_OPENGL_API);
+ else
+ NativeLibrary.eglBindAPI(EGL_OPENGL_ES_API);
+
+ mEGLContext = mEGL.eglCreateContext(mDisplay, mEGLConfigs[0], EGL10.EGL_NO_CONTEXT, ctx_attribs);
+ mEGLSurface = mEGL.eglCreatePbufferSurface(mDisplay, mEGLConfigs[0], attribs);
+ mEGL.eglMakeCurrent(mDisplay, mEGLSurface, mEGLSurface, mEGLContext);
+ mGL = (GL10) mEGLContext.getGL();
+ }
+
+ /**
+ * Simplified call to {@link GL10#glGetString(int)}
+ * <p>
+ * Accepts the following constants:
+ * <ul>
+ * <li>GL_VENDOR - Company responsible for the GL implementation.</li>
+ * <li>GL_VERSION - Version or release number.</li>
+ * <li>GL_RENDERER - Name of the renderer</li>
+ * <li>GL_SHADING_LANGUAGE_VERSION - Version or release number of the shading language </li>
+ * </ul>
+ *
+ * @param glEnum A symbolic constant within {@link GL10}.
+ *
+ * @return the string information represented by {@code glEnum}.
+ */
+ public String glGetString(int glEnum)
+ {
+ return mGL.glGetString(glEnum);
+ }
+
+ /**
+ * Simplified call to {@link GLES30#glGetStringi(int, int)}
+ * <p>
+ * Accepts the following constants:
+ * <ul>
+ * <li>GL_VENDOR - Company responsible for the GL implementation.</li>
+ * <li>GL_VERSION - Version or release number.</li>
+ * <li>GL_RENDERER - Name of the renderer</li>
+ * <li>GL_SHADING_LANGUAGE_VERSION - Version or release number of the shading language </li>
+ * <li>GL_EXTENSIONS - Extension string supported by the implementation at {@code index}.</li>
+ * </ul>
+ *
+ * @param glEnum A symbolic GL constant
+ * @param index The index of the string to return.
+ *
+ * @return the string information represented by {@code glEnum} and {@code index}.
+ */
+ public String glGetStringi(int glEnum, int index)
+ {
+ return GLES30.glGetStringi(glEnum, index);
+ }
+
+ public boolean SupportsExtension(String extension)
+ {
+ int[] num_ext = new int[1];
+ GLES30.glGetIntegerv(GLES30.GL_NUM_EXTENSIONS, num_ext, 0);
+
+ for (int i = 0; i < num_ext[0]; ++i)
+ {
+ String ext = GLES30.glGetStringi(GLES30.GL_EXTENSIONS, i);
+ if (ext.equals(extension))
+ return true;
+ }
+ return false;
+ }
+
+ public int GetVersion()
+ {
+ int[] major = new int[1];
+ int[] minor = new int[1];
+ GLES30.glGetIntegerv(GLES30.GL_MAJOR_VERSION, major, 0);
+ GLES30.glGetIntegerv(GLES30.GL_MINOR_VERSION, minor, 0);
+ return major[0] * 100 + minor[0] * 10;
+ }
+
+ /**
+ * Simplified call to {@link GL10#glGetIntegerv(int, int[], int)
+ *
+ * @param glEnum A symbolic GL constant.
+ *
+ * @return the integer information represented by {@code glEnum}.
+ */
+ public int glGetInteger(int glEnum)
+ {
+ int[] val = new int[1];
+ mGL.glGetIntegerv(glEnum, val, 0);
+ return val[0];
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/InputBindingPreference.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/InputBindingPreference.java
new file mode 100644
index 0000000000..9e651fc102
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/InputBindingPreference.java
@@ -0,0 +1,66 @@
+package org.dolphinemu.dolphinemu.utils;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.preference.EditTextPreference;
+import android.preference.Preference;
+import android.util.AttributeSet;
+
+import org.dolphinemu.dolphinemu.R;
+import org.dolphinemu.dolphinemu.dialogs.MotionAlertDialog;
+
+/**
+ * {@link Preference} subclass that represents a preference
+ * used for assigning a key bind.
+ */
+public final class InputBindingPreference extends EditTextPreference
+{
+ /**
+ * Constructor that is called when inflating an InputBindingPreference from XML.
+ *
+ * @param context The current {@link Context}.
+ * @param attrs The attributes of the XML tag that is inflating the preference.
+ */
+ public InputBindingPreference(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onClick()
+ {
+ // Begin the creation of the input alert.
+ final MotionAlertDialog dialog = new MotionAlertDialog(getContext(), this);
+
+ // Set the cancel button.
+ dialog.setButton(AlertDialog.BUTTON_NEGATIVE, getContext().getString(R.string.cancel), new AlertDialog.OnClickListener()
+ {
+ @Override
+ public void onClick(DialogInterface dialog, int which)
+ {
+ // Do nothing. Just makes the cancel button show up.
+ }
+ });
+
+ // Set the title and description message.
+ dialog.setTitle(R.string.input_binding);
+ dialog.setMessage(String.format(getContext().getString(R.string.input_binding_descrip), getTitle()));
+
+ // Don't allow the dialog to close when a user taps
+ // outside of it. They must press cancel or provide an input.
+ dialog.setCanceledOnTouchOutside(false);
+
+ // Everything is set, show the dialog.
+ dialog.show();
+ }
+
+ @Override
+ public CharSequence getSummary()
+ {
+ String summary = super.getSummary().toString();
+ return String.format(summary, getText());
+ }
+
+
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/SliderPreference.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/SliderPreference.java
new file mode 100644
index 0000000000..eab6f74ff0
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/SliderPreference.java
@@ -0,0 +1,94 @@
+package org.dolphinemu.dolphinemu.utils;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.preference.DialogPreference;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import org.dolphinemu.dolphinemu.R;
+
+public class SliderPreference extends DialogPreference implements SeekBar.OnSeekBarChangeListener, View.OnClickListener
+{
+ private static final String androidns = "http://schemas.android.com/apk/res/android";
+
+ // SeekBar
+ private int m_max, m_value;
+ private SeekBar m_seekbar;
+
+ // TextView
+ private TextView m_textview;
+
+ public SliderPreference(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+
+ // Seekbar values
+ m_value = attrs.getAttributeIntValue(androidns, "defaultValue", 0);
+ m_max = attrs.getAttributeIntValue(androidns, "max", 100);
+ }
+
+ @Override
+ protected View onCreateDialogView()
+ {
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+ LinearLayout layout = (LinearLayout)inflater.inflate(R.layout.slider_layout, null, false);
+
+ m_seekbar = (SeekBar)layout.findViewById(R.id.sliderSeekBar);
+ m_textview = (TextView)layout.findViewById(R.id.sliderTextView);
+
+ if (shouldPersist())
+ m_value = Integer.valueOf(getPersistedString(Integer.toString(m_value)));
+
+ m_seekbar.setMax(m_max);
+ m_seekbar.setProgress(m_value);
+ setProgressText(m_value);
+ m_seekbar.setOnSeekBarChangeListener(this);
+
+ return layout;
+ }
+
+ // SeekBar overrides
+ @Override
+ public void onProgressChanged(SeekBar seek, int value, boolean fromTouch)
+ {
+ m_value = value;
+ setProgressText(value);
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seek) {}
+ @Override
+ public void onStopTrackingTouch(SeekBar seek) {}
+
+ void setProgressText(int value)
+ {
+ m_textview.setText(String.valueOf(value));
+ }
+
+ @Override
+ public void showDialog(Bundle state)
+ {
+ super.showDialog(state);
+
+ Button positiveButton = ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_POSITIVE);
+ positiveButton.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View v)
+ {
+ if (shouldPersist())
+ {
+ persistString(Integer.toString(m_seekbar.getProgress()));
+ callChangeListener(m_seekbar.getProgress());
+ }
+ ((AlertDialog) getDialog()).dismiss();
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/UserPreferences.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/UserPreferences.java
new file mode 100644
index 0000000000..168bd2dd93
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/UserPreferences.java
@@ -0,0 +1,284 @@
+/**
+ * Copyright 2013 Dolphin Emulator Project
+ * Licensed under GPLv2+
+ * Refer to the license.txt file included.
+ */
+
+package org.dolphinemu.dolphinemu.utils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.preference.PreferenceManager;
+
+import org.dolphinemu.dolphinemu.NativeLibrary;
+
+/**
+ * A class that retrieves all of the set user preferences in Android, in a safe way.
+ * <p>
+ * If any preferences are added to this emulator, an accessor for that preference
+ * should be added here. This way lengthy calls to getters from SharedPreferences
+ * aren't made necessary.
+ */
+public final class UserPreferences
+{
+ private UserPreferences()
+ {
+ // Disallows instantiation.
+ }
+
+ /**
+ * Loads the settings stored in the Dolphin ini config files to the shared preferences of this front-end.
+ *
+ * @param ctx The context used to retrieve the SharedPreferences instance.
+ */
+ public static void LoadIniToPrefs(Context ctx)
+ {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
+
+ // Get an editor.
+ SharedPreferences.Editor editor = prefs.edit();
+
+ // Add the settings.
+ if (Build.CPU_ABI.contains("arm64"))
+ editor.putString("cpuCorePref", getConfig("Dolphin.ini", "Core", "CPUCore", "4"));
+ else
+ editor.putString("cpuCorePref", getConfig("Dolphin.ini", "Core", "CPUCore", "3"));
+
+ editor.putBoolean("dualCorePref", getConfig("Dolphin.ini", "Core", "CPUThread", "False").equals("True"));
+ editor.putBoolean("fastmemPref", getConfig("Dolphin.ini", "Core", "Fastmem", "False").equals("True"));
+
+ editor.putString("gpuPref", getConfig("Dolphin.ini", "Core", "GFXBackend", "OGL"));
+ editor.putBoolean("showFPS", getConfig("gfx_opengl.ini", "Settings", "ShowFPS", "False").equals("True"));
+ editor.putBoolean("drawOnscreenControls", getConfig("Dolphin.ini", "Android", "ScreenControls", "True").equals("True"));
+
+ editor.putString("internalResolution", getConfig("gfx_opengl.ini", "Settings", "EFBScale", "2") );
+ editor.putString("FSAA", getConfig("gfx_opengl.ini", "Settings", "MSAA", "0"));
+ editor.putString("anisotropicFiltering", getConfig("gfx_opengl.ini", "Enhancements", "MaxAnisotropy", "0"));
+ editor.putString("postProcessingShader", getConfig("gfx_opengl.ini", "Enhancements", "PostProcessingShader", ""));
+ editor.putBoolean("scaledEFBCopy", getConfig("gfx_opengl.ini", "Hacks", "EFBScaledCopy", "True").equals("True"));
+ editor.putBoolean("perPixelLighting", getConfig("gfx_opengl.ini", "Settings", "EnablePixelLighting", "False").equals("True"));
+ editor.putBoolean("forceTextureFiltering", getConfig("gfx_opengl.ini", "Enhancements", "ForceFiltering", "False").equals("True"));
+ editor.putBoolean("disableFog", getConfig("gfx_opengl.ini", "Settings", "DisableFog", "False").equals("True"));
+ editor.putBoolean("skipEFBAccess", getConfig("gfx_opengl.ini", "Hacks", "EFBAccessEnable", "False").equals("True"));
+ editor.putBoolean("ignoreFormatChanges", getConfig("gfx_opengl.ini", "Hacks", "EFBEmulateFormatChanges", "False").equals("True"));
+ editor.putString("stereoscopyMode", getConfig("gfx_opengl.ini", "Enhancements", "StereoMode", "0"));
+ editor.putBoolean("stereoSwapEyes", getConfig("gfx_opengl.ini", "Enhancements", "StereoSwapEyes", "False").equals("True"));
+ editor.putString("stereoDepth", getConfig("gfx_opengl.ini", "Enhancements", "StereoDepth", "20"));
+ editor.putString("stereoConvergence", getConfig("gfx_opengl.ini", "Enhancements", "StereoConvergence", "20"));
+
+ String efbCopyOn = getConfig("gfx_opengl.ini", "Hacks", "EFBCopyEnable", "True");
+ String efbToTexture = getConfig("gfx_opengl.ini", "Hacks", "EFBToTextureEnable", "True");
+ String efbCopyCache = getConfig("gfx_opengl.ini", "Hacks", "EFBCopyCacheEnable", "False");
+
+ if (efbCopyOn.equals("False"))
+ {
+ editor.putString("efbCopyMethod", "Off");
+ }
+ else if (efbCopyOn.equals("True") && efbToTexture.equals("True"))
+ {
+ editor.putString("efbCopyMethod", "Texture");
+ }
+ else if(efbCopyOn.equals("True") && efbToTexture.equals("False") && efbCopyCache.equals("False"))
+ {
+ editor.putString("efbCopyMethod", "RAM (uncached)");
+ }
+ else if(efbCopyOn.equals("True") && efbToTexture.equals("False") && efbCopyCache.equals("True"))
+ {
+ editor.putString("efbCopyMethod", "RAM (cached)");
+ }
+
+ editor.putString("textureCacheAccuracy", getConfig("gfx_opengl.ini", "Settings", "SafeTextureCacheColorSamples", "128"));
+
+ String usingXFB = getConfig("gfx_opengl.ini", "Settings", "UseXFB", "False");
+ String usingRealXFB = getConfig("gfx_opengl.ini", "Settings", "UseRealXFB", "False");
+
+ if (usingXFB.equals("False"))
+ {
+ editor.putString("externalFrameBuffer", "Disabled");
+ }
+ else if (usingXFB.equals("True") && usingRealXFB.equals("False"))
+ {
+ editor.putString("externalFrameBuffer", "Virtual");
+ }
+ else if (usingXFB.equals("True") && usingRealXFB.equals("True"))
+ {
+ editor.putString("externalFrameBuffer", "Real");
+ }
+
+ editor.putBoolean("disableDestinationAlpha", getConfig("gfx_opengl.ini", "Settings", "DstAlphaPass", "False").equals("True"));
+ editor.putBoolean("fastDepthCalculation", getConfig("gfx_opengl.ini", "Settings", "FastDepthCalc", "True").equals("True"));
+ editor.putString("aspectRatio", getConfig("gfx_opengl.ini", "Settings", "AspectRatio", "0"));
+
+ // Apply the changes.
+ editor.apply();
+ }
+
+ // Small utility method that shortens calls to NativeLibrary.GetConfig.
+ private static String getConfig(String ini, String section, String key, String defaultValue)
+ {
+ return NativeLibrary.GetConfig(ini, section, key, defaultValue);
+ }
+
+ /**
+ * Writes the preferences set in the front-end to the Dolphin ini files.
+ *
+ * @param ctx The context used to retrieve the user settings.
+ * */
+ public static void SavePrefsToIni(Context ctx)
+ {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
+
+ // Whether or not the user is using dual core.
+ boolean isUsingDualCore = prefs.getBoolean("dualCorePref", true);
+
+ // Current CPU core being used. Falls back to interpreter upon error.
+ String currentEmuCore = prefs.getString("cpuCorePref", "0");
+
+ // Fastmem JIT core usage
+ boolean isUsingFastmem = prefs.getBoolean("fastmemPref", false);
+
+ // Current video backend being used. Falls back to software rendering upon error.
+ String currentVideoBackend = prefs.getString("gpuPref", "Software Rendering");
+
+ // Whether or not FPS will be displayed on-screen.
+ boolean showingFPS = prefs.getBoolean("showFPS", false);
+
+ // Whether or not to draw on-screen controls.
+ boolean drawingOnscreenControls = prefs.getBoolean("drawOnscreenControls", true);
+
+ // Whether or not to ignore all EFB access requests from the CPU.
+ boolean skipEFBAccess = prefs.getBoolean("skipEFBAccess", false);
+
+ // Whether or not to ignore changes to the EFB format.
+ boolean ignoreFormatChanges = prefs.getBoolean("ignoreFormatChanges", false);
+
+ // EFB copy method to use.
+ String efbCopyMethod = prefs.getString("efbCopyMethod", "Texture");
+
+ // Texture cache accuracy. Falls back to "Fast" up error.
+ String textureCacheAccuracy = prefs.getString("textureCacheAccuracy", "128");
+
+ // External frame buffer emulation. Falls back to disabled upon error.
+ String externalFrameBuffer = prefs.getString("externalFrameBuffer", "Disabled");
+
+ // Whether or not to disable destination alpha.
+ boolean disableDstAlphaPass = prefs.getBoolean("disableDestinationAlpha", false);
+
+ // Whether or not to use fast depth calculation.
+ boolean useFastDepthCalc = prefs.getBoolean("fastDepthCalculation", true);
+
+ // Aspect ratio selection
+ String aspectRatio = prefs.getString("aspectRatio", "0");
+
+ // Internal resolution. Falls back to 1x Native upon error.
+ String internalResolution = prefs.getString("internalResolution", "2");
+
+ // FSAA Level. Falls back to 1x upon error.
+ String FSAALevel = prefs.getString("FSAA", "0");
+
+ // Anisotropic Filtering Level. Falls back to 1x upon error.
+ String anisotropicFiltLevel = prefs.getString("anisotropicFiltering", "0");
+
+ // Post processing shader setting
+ String postProcessing = prefs.getString("postProcessingShader", "");
+
+ // Whether or not Scaled EFB copies are used.
+ boolean usingScaledEFBCopy = prefs.getBoolean("scaledEFBCopy", true);
+
+ // Whether or not per-pixel lighting is used.
+ boolean usingPerPixelLighting = prefs.getBoolean("perPixelLighting", false);
+
+ // Whether or not texture filtering is being forced.
+ boolean isForcingTextureFiltering = prefs.getBoolean("forceTextureFiltering", false);
+
+ // Whether or not fog is disabled.
+ boolean fogIsDisabled = prefs.getBoolean("disableFog", false);
+
+ // Stereoscopy setting
+ String stereoscopyMode = prefs.getString("stereoscopyMode", "0");
+
+ // Stereoscopy swap eyes
+ boolean stereoscopyEyeSwap = prefs.getBoolean("stereoSwapEyes", false);
+
+ // Stereoscopy separation
+ String stereoscopySeparation = prefs.getString("stereoDepth", "20");
+
+ // Stereoscopy convergence
+ String stereoscopyConvergence = prefs.getString("stereoConvergence", "20");
+
+ // CPU related Settings
+ NativeLibrary.SetConfig("Dolphin.ini", "Core", "CPUCore", currentEmuCore);
+ NativeLibrary.SetConfig("Dolphin.ini", "Core", "CPUThread", isUsingDualCore ? "True" : "False");
+ NativeLibrary.SetConfig("Dolphin.ini", "Core", "Fastmem", isUsingFastmem ? "True" : "False");
+
+ // General Video Settings
+ NativeLibrary.SetConfig("Dolphin.ini", "Core", "GFXBackend", currentVideoBackend);
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Settings", "ShowFPS", showingFPS ? "True" : "False");
+ NativeLibrary.SetConfig("Dolphin.ini", "Android", "ScreenControls", drawingOnscreenControls ? "True" : "False");
+
+ // Video Hack Settings
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Hacks", "EFBAccessEnable", skipEFBAccess ? "False" : "True");
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Hacks", "EFBEmulateFormatChanges", ignoreFormatChanges ? "True" : "False");
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Settings", "AspectRatio", aspectRatio);
+
+ // Set EFB Copy Method
+ if (efbCopyMethod.equals("Off"))
+ {
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Hacks", "EFBCopyEnable", "False");
+ }
+ else if (efbCopyMethod.equals("Texture"))
+ {
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Hacks", "EFBCopyEnable", "True");
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Hacks", "EFBToTextureEnable", "True");
+ }
+ else if (efbCopyMethod.equals("RAM (uncached)"))
+ {
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Hacks", "EFBCopyEnable", "True");
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Hacks", "EFBToTextureEnable", "False");
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Hacks", "EFBCopyCacheEnable", "False");
+ }
+ else if (efbCopyMethod.equals("RAM (cached)"))
+ {
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Hacks", "EFBCopyEnable", "True");
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Hacks", "EFBToTextureEnable", "False");
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Hacks", "EFBCopyCacheEnable", "True");
+ }
+
+ // Set texture cache accuracy
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Settings", "SafeTextureCacheColorSamples", textureCacheAccuracy);
+
+ // Set external frame buffer.
+ if (externalFrameBuffer.equals("Disabled"))
+ {
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Settings", "UseXFB", "False");
+ }
+ else if (externalFrameBuffer.equals("Virtual"))
+ {
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Settings", "UseXFB", "True");
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Settings", "UseRealXFB", "False");
+ }
+ else if (externalFrameBuffer.equals("Real"))
+ {
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Settings", "UseXFB", "True");
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Settings", "UseRealXFB", "True");
+ }
+
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Settings", "DstAlphaPass", disableDstAlphaPass ? "True" : "False");
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Settings", "FastDepthCalc", useFastDepthCalc ? "True" : "False");
+
+ //-- Enhancement Settings --//
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Settings", "EFBScale", internalResolution);
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Settings", "MSAA", FSAALevel);
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Enhancements", "MaxAnisotropy", anisotropicFiltLevel);
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Enhancements", "PostProcessingShader", postProcessing);
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Hacks", "EFBScaledCopy", usingScaledEFBCopy ? "True" : "False");
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Settings", "EnablePixelLighting", usingPerPixelLighting ? "True" : "False");
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Enhancements", "ForceFiltering", isForcingTextureFiltering ? "True" : "False");
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Settings", "DisableFog", fogIsDisabled ? "True" : "False");
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Enhancements", "StereoMode", stereoscopyMode);
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Enhancements", "StereoSwapEyes", stereoscopyEyeSwap ? "True" : "False");
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Enhancements", "StereoDepth", stereoscopySeparation);
+ NativeLibrary.SetConfig("gfx_opengl.ini", "Enhancements", "StereoConvergence", stereoscopyConvergence);
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/FileViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/FileViewHolder.java
new file mode 100644
index 0000000000..902e5299f4
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/FileViewHolder.java
@@ -0,0 +1,30 @@
+package org.dolphinemu.dolphinemu.viewholders;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+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 View itemView;
+
+ public TextView textFileName;
+ public ImageView imageType;
+
+ public FileViewHolder(View itemView)
+ {
+ super(itemView);
+
+ this.itemView = itemView;
+
+ textFileName = (TextView) itemView.findViewById(R.id.text_file_name);
+ imageType = (ImageView) itemView.findViewById(R.id.image_type);
+ }
+}
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
new file mode 100644
index 0000000000..592a2ecf32
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/GameViewHolder.java
@@ -0,0 +1,40 @@
+package org.dolphinemu.dolphinemu.viewholders;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.dolphinemu.dolphinemu.R;
+
+/**
+ * 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 ImageView imageScreenshot;
+ 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 GameViewHolder(View itemView)
+ {
+ super(itemView);
+
+ 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);
+ }
+}