diff --git a/Source/Android/app/src/main/AndroidManifest.xml b/Source/Android/app/src/main/AndroidManifest.xml index 32233600d0..78548b4c6c 100644 --- a/Source/Android/app/src/main/AndroidManifest.xml +++ b/Source/Android/app/src/main/AndroidManifest.xml @@ -47,6 +47,10 @@ + + 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..de7ebb17d3 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java @@ -0,0 +1,292 @@ +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.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 org.dolphinemu.dolphinemu.settings.input.InputConfigFragment; + +import java.util.List; + +public final class EmulationActivity extends Activity +{ + private View mDecorView; + + /** + * 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); + + // 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) + { + boolean visible = (flags & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0; + + if (visible) + { + 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 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 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(InputConfigFragment.getInputDesc(input), 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 motions = input.getMotionRanges(); + + for (InputDevice.MotionRange range : motions) + { + NativeLibrary.onGamePadMoveEvent(InputConfigFragment.getInputDesc(input), 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() + { + 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() + { + mDecorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + } +} 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 index 0f964e87f4..d296c5e023 100644 --- 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 @@ -14,8 +14,8 @@ 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.emulation.EmulationActivity; import org.dolphinemu.dolphinemu.model.GameDatabase; import org.dolphinemu.dolphinemu.viewholders.GameViewHolder; @@ -215,6 +215,7 @@ public final class GameAdapter extends RecyclerView.Adapter impl Intent intent = new Intent(view.getContext(), EmulationActivity.class); intent.putExtra("SelectedGame", holder.path); + intent.putExtra("SelectedTitle", holder.title); view.getContext().startActivity(intent); } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.java index 34d40048b4..04c7253cb5 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.java @@ -16,7 +16,7 @@ import com.squareup.picasso.Picasso; import org.dolphinemu.dolphinemu.BuildConfig; import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.emulation.EmulationActivity; +import org.dolphinemu.dolphinemu.activities.EmulationActivity; import de.hdodenhof.circleimageview.CircleImageView; @@ -79,6 +79,8 @@ public final class GameDetailsDialog extends DialogFragment 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); } }); 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..7c83c9d977 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.java @@ -0,0 +1,114 @@ +package org.dolphinemu.dolphinemu.fragments; + +import android.app.Fragment; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.view.LayoutInflater; +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.emulation.overlay.InputOverlay; + + +public final class EmulationFragment extends Fragment +{ + 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 InputOverlay mInputOverlay; + + 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); + + mPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); + } + + /** + * Initialize the UI and start emulation in here. + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + { + String path = getArguments().getString(ARGUMENT_GAME_PATH); + + View contents = inflater.inflate(R.layout.fragment_emulation, container, false); + + mInputOverlay = (InputOverlay) contents.findViewById(R.id.surface_input_overlay); + + NativeLibrary.SetFilename(path); + + + // If the input overlay was previously disabled, then don't show it. + if (!mPreferences.getBoolean("showInputOverlay", true)) + { + mInputOverlay.setVisibility(View.GONE); + } + + return contents; + } + + @Override + public void onStart() + { + super.onStart(); + NativeLibrary.UnPauseEmulation(); + } + + @Override + public void onStop() + { + super.onStop(); + NativeLibrary.PauseEmulation(); + } + + @Override + public void onDestroyView() + { + super.onDestroyView(); + 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(); + } +} diff --git a/Source/Android/app/src/main/res/layout/activity_emulation.xml b/Source/Android/app/src/main/res/layout/activity_emulation.xml new file mode 100644 index 0000000000..63ea99ba83 --- /dev/null +++ b/Source/Android/app/src/main/res/layout/activity_emulation.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/Source/Android/app/src/main/res/layout/activity_game_grid.xml b/Source/Android/app/src/main/res/layout/activity_game_grid.xml index 8186490004..53ac5d614e 100644 --- a/Source/Android/app/src/main/res/layout/activity_game_grid.xml +++ b/Source/Android/app/src/main/res/layout/activity_game_grid.xml @@ -9,7 +9,7 @@ android:id="@+id/toolbar_game_list" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@color/dolphin_blue" + android:background="?android:colorPrimary" android:minHeight="?android:attr/actionBarSize" android:theme="@android:style/ThemeOverlay.Material.Dark.ActionBar" android:elevation="6dp"/> diff --git a/Source/Android/app/src/main/res/layout/fragment_emulation.xml b/Source/Android/app/src/main/res/layout/fragment_emulation.xml new file mode 100644 index 0000000000..6a936dae64 --- /dev/null +++ b/Source/Android/app/src/main/res/layout/fragment_emulation.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/Source/Android/app/src/main/res/menu/menu_emulation.xml b/Source/Android/app/src/main/res/menu/menu_emulation.xml new file mode 100644 index 0000000000..627d1bc9e3 --- /dev/null +++ b/Source/Android/app/src/main/res/menu/menu_emulation.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 07e5ab3690..47fefbaf20 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -236,4 +236,7 @@ CPU Settings Input Settings Video Settings + Emulation Activity + + Toggle Input Overlay diff --git a/Source/Android/app/src/main/res/values/styles.xml b/Source/Android/app/src/main/res/values/styles.xml index 2752458eb2..bec7fc0035 100644 --- a/Source/Android/app/src/main/res/values/styles.xml +++ b/Source/Android/app/src/main/res/values/styles.xml @@ -22,23 +22,19 @@ @@ -49,26 +45,39 @@ + + + + + + + + + \ No newline at end of file