diff options
| author | Jules Blok <jules.blok@gmail.com> | 2015-06-10 00:00:12 +0200 |
|---|---|---|
| committer | Jules Blok <jules.blok@gmail.com> | 2015-06-10 00:00:12 +0200 |
| commit | 7dfced21a2e5aac7195e0e1ad76468e30306766b (patch) | |
| tree | bd671b639d3d911460b767caabad8fc8a759a6bf /Source/Android/app/src/main/java | |
| parent | c25be031fc1031d795157d8344b87b7841145197 (diff) | |
| parent | 7b0a65e295c784f9d573f2ef52d4e2a7b9bb2889 (diff) | |
Merge branch 'master' into stable
Diffstat (limited to 'Source/Android/app/src/main/java')
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); + } +} |
