From 39a65f8446d9301e48b40079707e075495336356 Mon Sep 17 00:00:00 2001
From: Charles Lombardo <clombardo169@gmail.com>
Date: Tue, 7 Mar 2023 13:19:05 -0500
Subject: [PATCH] android: Convert EmulationActivity to Kotlin

---
 .../activities/EmulationActivity.java         | 347 ------------------
 .../yuzu_emu/activities/EmulationActivity.kt  | 286 +++++++++++++++
 2 files changed, 286 insertions(+), 347 deletions(-)
 delete mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.java
 create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt

diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.java
deleted file mode 100644
index 343bc032b8..0000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.java
+++ /dev/null
@@ -1,347 +0,0 @@
-package org.yuzu.yuzu_emu.activities;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.graphics.Rect;
-import android.os.Bundle;
-import android.os.Handler;
-import android.preference.PreferenceManager;
-import android.view.InputDevice;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.WindowManager;
-import android.widget.SeekBar;
-import android.widget.TextView;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.core.app.NotificationManagerCompat;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentActivity;
-import androidx.fragment.app.FragmentManager;
-
-import org.yuzu.yuzu_emu.NativeLibrary;
-import org.yuzu.yuzu_emu.R;
-import org.yuzu.yuzu_emu.fragments.EmulationFragment;
-import org.yuzu.yuzu_emu.fragments.MenuFragment;
-import org.yuzu.yuzu_emu.utils.ControllerMappingHelper;
-import org.yuzu.yuzu_emu.utils.ForegroundService;
-
-import java.lang.annotation.Retention;
-
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-public final class EmulationActivity extends AppCompatActivity {
-    private static final String BACKSTACK_NAME_MENU = "menu";
-
-    private static final String BACKSTACK_NAME_SUBMENU = "submenu";
-
-    public static final String EXTRA_SELECTED_GAME = "SelectedGame";
-    public static final String EXTRA_SELECTED_TITLE = "SelectedTitle";
-    public static final int MENU_ACTION_EDIT_CONTROLS_PLACEMENT = 0;
-    public static final int MENU_ACTION_TOGGLE_CONTROLS = 1;
-    public static final int MENU_ACTION_ADJUST_SCALE = 2;
-    public static final int MENU_ACTION_EXIT = 3;
-    public static final int MENU_ACTION_SHOW_FPS = 4;
-    public static final int MENU_ACTION_RESET_OVERLAY = 6;
-    public static final int MENU_ACTION_SHOW_OVERLAY = 7;
-    public static final int MENU_ACTION_OPEN_SETTINGS = 8;
-    private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000;
-    private View mDecorView;
-    private EmulationFragment mEmulationFragment;
-    private SharedPreferences mPreferences;
-    private ControllerMappingHelper mControllerMappingHelper;
-    // TODO(bunnei): Disable notifications until we support app suspension.
-//    private Intent foregroundService;
-    private boolean activityRecreated;
-    private String mSelectedTitle;
-    private String mPath;
-
-    private boolean mMenuVisible;
-
-    public static void launch(FragmentActivity activity, String path, String title) {
-        Intent launcher = new Intent(activity, EmulationActivity.class);
-
-        launcher.putExtra(EXTRA_SELECTED_GAME, path);
-        launcher.putExtra(EXTRA_SELECTED_TITLE, title);
-        activity.startActivity(launcher);
-    }
-
-    public static void tryDismissRunningNotification(Activity activity) {
-        // TODO(bunnei): Disable notifications until we support app suspension.
-//        NotificationManagerCompat.from(activity).cancel(EMULATION_RUNNING_NOTIFICATION);
-    }
-
-    @Override
-    protected void onDestroy() {
-        // TODO(bunnei): Disable notifications until we support app suspension.
-//        stopService(foregroundService);
-        super.onDestroy();
-    }
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        if (savedInstanceState == null) {
-            // Get params we were passed
-            Intent gameToEmulate = getIntent();
-            mPath = gameToEmulate.getStringExtra(EXTRA_SELECTED_GAME);
-            mSelectedTitle = gameToEmulate.getStringExtra(EXTRA_SELECTED_TITLE);
-            activityRecreated = false;
-        } else {
-            activityRecreated = true;
-            restoreState(savedInstanceState);
-        }
-
-        mControllerMappingHelper = new ControllerMappingHelper();
-
-        // Get a handle to the Window containing the UI.
-        mDecorView = getWindow().getDecorView();
-        mDecorView.setOnSystemUiVisibilityChangeListener(visibility ->
-        {
-            if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
-                // Go back to immersive fullscreen mode in 3s
-                Handler handler = new Handler(getMainLooper());
-                handler.postDelayed(this::enableFullscreenImmersive, 3000 /* 3s */);
-            }
-        });
-        // Set these options now so that the SurfaceView the game renders into is the right size.
-        enableFullscreenImmersive();
-
-        setTheme(R.style.YuzuEmulationBase);
-
-        setContentView(R.layout.activity_emulation);
-
-        // Find or create the EmulationFragment
-        mEmulationFragment = (EmulationFragment) getSupportFragmentManager()
-                .findFragmentById(R.id.frame_emulation_fragment);
-        if (mEmulationFragment == null) {
-            mEmulationFragment = EmulationFragment.newInstance(mPath);
-            getSupportFragmentManager().beginTransaction()
-                    .add(R.id.frame_emulation_fragment, mEmulationFragment)
-                    .commit();
-        }
-
-        setTitle(mSelectedTitle);
-
-        mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
-
-        // Start a foreground service to prevent the app from getting killed in the background
-        // TODO(bunnei): Disable notifications until we support app suspension.
-//        foregroundService = new Intent(EmulationActivity.this, ForegroundService.class);
-//        startForegroundService(foregroundService);
-    }
-
-    @Override
-    protected void onSaveInstanceState(@NonNull Bundle outState) {
-        outState.putString(EXTRA_SELECTED_GAME, mPath);
-        outState.putString(EXTRA_SELECTED_TITLE, mSelectedTitle);
-        super.onSaveInstanceState(outState);
-    }
-
-    protected void restoreState(Bundle savedInstanceState) {
-        mPath = savedInstanceState.getString(EXTRA_SELECTED_GAME);
-        mSelectedTitle = savedInstanceState.getString(EXTRA_SELECTED_TITLE);
-
-        // If an alert prompt was in progress when state was restored, retry displaying it
-        NativeLibrary.retryDisplayAlertPrompt();
-    }
-
-    @Override
-    public void onRestart() {
-        super.onRestart();
-    }
-
-    @Override
-    public void onBackPressed() {
-        toggleMenu();
-    }
-
-    private void enableFullscreenImmersive() {
-        getWindow().getAttributes().layoutInDisplayCutoutMode=
-                WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
-
-        // It would be nice to use IMMERSIVE_STICKY, but that doesn't show the toolbar.
-        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);
-    }
-
-    public void handleMenuAction(int action) {
-        switch (action) {
-            case MENU_ACTION_EXIT:
-                mEmulationFragment.stopEmulation();
-                finish();
-                break;
-        }
-    }
-
-    private void editControlsPlacement() {
-        if (mEmulationFragment.isConfiguringControls()) {
-            mEmulationFragment.stopConfiguringControls();
-        } else {
-            mEmulationFragment.startConfiguringControls();
-        }
-    }
-
-    private void adjustScale() {
-        LayoutInflater inflater = LayoutInflater.from(this);
-        View view = inflater.inflate(R.layout.dialog_seekbar, null);
-
-        final SeekBar seekbar = view.findViewById(R.id.seekbar);
-        final TextView value = view.findViewById(R.id.text_value);
-        final TextView units = view.findViewById(R.id.text_units);
-
-        seekbar.setMax(150);
-        seekbar.setProgress(mPreferences.getInt("controlScale", 50));
-        seekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
-            public void onStartTrackingTouch(SeekBar seekBar) {
-            }
-
-            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
-                value.setText(String.valueOf(progress + 50));
-            }
-
-            public void onStopTrackingTouch(SeekBar seekBar) {
-                setControlScale(seekbar.getProgress());
-            }
-        });
-
-        value.setText(String.valueOf(seekbar.getProgress() + 50));
-        units.setText("%");
-
-        AlertDialog.Builder builder = new AlertDialog.Builder(this);
-        builder.setTitle(R.string.emulation_control_scale);
-        builder.setView(view);
-        final int previousProgress = seekbar.getProgress();
-        builder.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> {
-            setControlScale(previousProgress);
-        });
-        builder.setPositiveButton(android.R.string.ok, (dialogInterface, i) ->
-        {
-            setControlScale(seekbar.getProgress());
-        });
-        builder.setNeutralButton(R.string.slider_default, (dialogInterface, i) -> {
-            setControlScale(50);
-        });
-
-        AlertDialog alertDialog = builder.create();
-        alertDialog.show();
-    }
-
-    private void setControlScale(int scale) {
-        SharedPreferences.Editor editor = mPreferences.edit();
-        editor.putInt("controlScale", scale);
-        editor.apply();
-        mEmulationFragment.refreshInputOverlay();
-    }
-
-    private void resetOverlay() {
-        new AlertDialog.Builder(this)
-                .setTitle(getString(R.string.emulation_touch_overlay_reset))
-                .setPositiveButton(android.R.string.yes, (dialogInterface, i) -> mEmulationFragment.resetInputOverlay())
-                .setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> {
-                })
-                .create()
-                .show();
-    }
-
-    private static boolean areCoordinatesOutside(@Nullable View view, float x, float y)
-    {
-        if (view == null)
-        {
-            return true;
-        }
-
-        Rect viewBounds = new Rect();
-        view.getGlobalVisibleRect(viewBounds);
-        return !viewBounds.contains(Math.round(x), Math.round(y));
-    }
-
-    @Override
-    public boolean dispatchTouchEvent(MotionEvent event)
-    {
-        if (event.getActionMasked() == MotionEvent.ACTION_DOWN)
-        {
-            boolean anyMenuClosed = false;
-
-            Fragment submenu = getSupportFragmentManager().findFragmentById(R.id.frame_submenu);
-            if (submenu != null && areCoordinatesOutside(submenu.getView(), event.getX(), event.getY()))
-            {
-                closeSubmenu();
-                submenu = null;
-                anyMenuClosed = true;
-            }
-
-            if (submenu == null)
-            {
-                Fragment menu = getSupportFragmentManager().findFragmentById(R.id.frame_menu);
-                if (menu != null && areCoordinatesOutside(menu.getView(), event.getX(), event.getY()))
-                {
-                    closeMenu();
-                    anyMenuClosed = true;
-                }
-            }
-
-            if (anyMenuClosed)
-            {
-                return true;
-            }
-        }
-
-        return super.dispatchTouchEvent(event);
-    }
-
-    public boolean isActivityRecreated() {
-        return activityRecreated;
-    }
-
-    @Retention(SOURCE)
-    @IntDef({MENU_ACTION_EDIT_CONTROLS_PLACEMENT, MENU_ACTION_TOGGLE_CONTROLS, MENU_ACTION_ADJUST_SCALE,
-            MENU_ACTION_EXIT, MENU_ACTION_SHOW_FPS, MENU_ACTION_RESET_OVERLAY, MENU_ACTION_SHOW_OVERLAY, MENU_ACTION_OPEN_SETTINGS})
-    public @interface MenuAction {
-    }
-
-    private boolean closeSubmenu()
-    {
-        return getSupportFragmentManager().popBackStackImmediate(BACKSTACK_NAME_SUBMENU,
-                FragmentManager.POP_BACK_STACK_INCLUSIVE);
-    }
-
-    private boolean closeMenu()
-    {
-        mMenuVisible = false;
-        return getSupportFragmentManager().popBackStackImmediate(BACKSTACK_NAME_MENU,
-                FragmentManager.POP_BACK_STACK_INCLUSIVE);
-    }
-
-    private void toggleMenu()
-    {
-        if (!closeMenu()) {
-            // Removing the menu failed, so that means it wasn't visible. Add it.
-            Fragment fragment = MenuFragment.newInstance();
-            getSupportFragmentManager().beginTransaction()
-                    .setCustomAnimations(
-                            R.animator.menu_slide_in_from_start,
-                            R.animator.menu_slide_out_to_start,
-                            R.animator.menu_slide_in_from_start,
-                            R.animator.menu_slide_out_to_start)
-                    .add(R.id.frame_menu, fragment)
-                    .addToBackStack(BACKSTACK_NAME_MENU)
-                    .commit();
-            mMenuVisible = true;
-        }
-    }
-
-}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
new file mode 100644
index 0000000000..bd71a36532
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
@@ -0,0 +1,286 @@
+package org.yuzu.yuzu_emu.activities
+
+import android.app.Activity
+import android.content.DialogInterface
+import android.content.Intent
+import android.graphics.Rect
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.WindowManager
+import android.widget.TextView
+import androidx.activity.OnBackPressedCallback
+import androidx.annotation.IntDef
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import androidx.fragment.app.FragmentManager
+import androidx.preference.PreferenceManager
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.slider.Slider
+import com.google.android.material.slider.Slider.OnChangeListener
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.fragments.EmulationFragment
+import org.yuzu.yuzu_emu.fragments.MenuFragment
+import org.yuzu.yuzu_emu.utils.ControllerMappingHelper
+import kotlin.math.roundToInt
+
+open class EmulationActivity : AppCompatActivity() {
+    private var controllerMappingHelper: ControllerMappingHelper? = null
+
+    // TODO(bunnei): Disable notifications until we support app suspension.
+    //private Intent foregroundService;
+
+    var isActivityRecreated = false
+    private var selectedTitle: String? = null
+    private var path: String? = null
+    private var menuVisible = false
+    private var emulationFragment: EmulationFragment? = null
+
+    override fun onDestroy() {
+        // TODO(bunnei): Disable notifications until we support app suspension.
+        //stopService(foregroundService);
+        super.onDestroy()
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        if (savedInstanceState == null) {
+            // Get params we were passed
+            val gameToEmulate = intent
+            path = gameToEmulate.getStringExtra(EXTRA_SELECTED_GAME)
+            selectedTitle = gameToEmulate.getStringExtra(EXTRA_SELECTED_TITLE)
+            isActivityRecreated = false
+        } else {
+            isActivityRecreated = true
+            restoreState(savedInstanceState)
+        }
+        controllerMappingHelper = ControllerMappingHelper()
+
+        // Set these options now so that the SurfaceView the game renders into is the right size.
+        enableFullscreenImmersive()
+
+        setContentView(R.layout.activity_emulation)
+
+        // Find or create the EmulationFragment
+        var emulationFragment =
+            supportFragmentManager.findFragmentById(R.id.frame_emulation_fragment) as EmulationFragment?
+        if (emulationFragment == null) {
+            emulationFragment = EmulationFragment.newInstance(path)
+            supportFragmentManager.beginTransaction()
+                .add(R.id.frame_emulation_fragment, emulationFragment)
+                .commit()
+        }
+        title = selectedTitle
+
+        // Start a foreground service to prevent the app from getting killed in the background
+        // TODO(bunnei): Disable notifications until we support app suspension.
+        //foregroundService = new Intent(EmulationActivity.this, ForegroundService.class);
+        //startForegroundService(foregroundService);
+
+        onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
+            override fun handleOnBackPressed() {
+                toggleMenu()
+            }
+        })
+    }
+
+    override fun onSaveInstanceState(outState: Bundle) {
+        outState.putString(EXTRA_SELECTED_GAME, path)
+        outState.putString(EXTRA_SELECTED_TITLE, selectedTitle)
+        super.onSaveInstanceState(outState)
+    }
+
+    private fun restoreState(savedInstanceState: Bundle) {
+        path = savedInstanceState.getString(EXTRA_SELECTED_GAME)
+        selectedTitle = savedInstanceState.getString(EXTRA_SELECTED_TITLE)
+
+        // If an alert prompt was in progress when state was restored, retry displaying it
+        NativeLibrary.retryDisplayAlertPrompt()
+    }
+
+    private fun enableFullscreenImmersive() {
+        window.attributes.layoutInDisplayCutoutMode =
+            WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
+
+        // It would be nice to use IMMERSIVE_STICKY, but that doesn't show the toolbar.
+        window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
+                View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
+                View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
+                View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
+                View.SYSTEM_UI_FLAG_FULLSCREEN or
+                View.SYSTEM_UI_FLAG_IMMERSIVE
+    }
+
+    fun handleMenuAction(action: Int) {
+        when (action) {
+            MENU_ACTION_EXIT -> {
+                emulationFragment!!.stopEmulation()
+                finish()
+            }
+        }
+    }
+
+    private fun editControlsPlacement() {
+        if (emulationFragment!!.isConfiguringControls) {
+            emulationFragment!!.stopConfiguringControls()
+        } else {
+            emulationFragment!!.startConfiguringControls()
+        }
+    }
+
+    private fun adjustScale() {
+        val inflater = LayoutInflater.from(this)
+        val view = inflater.inflate(R.layout.dialog_slider, null)
+        val slider = view.findViewById<Slider>(R.id.slider)
+        val textValue = view.findViewById<TextView>(R.id.text_value)
+        val units = view.findViewById<TextView>(R.id.text_units)
+
+        slider.valueTo = 150F
+        slider.value = PreferenceManager.getDefaultSharedPreferences(applicationContext)
+            .getInt(Settings.PREF_CONTROL_SCALE, 50).toFloat()
+        slider.addOnChangeListener(OnChangeListener { _, value, _ ->
+            textValue.text = value.toString()
+            setControlScale(value.toInt())
+        })
+        textValue.text = slider.value.toString()
+        units.text = "%"
+        MaterialAlertDialogBuilder(this)
+            .setTitle(R.string.emulation_control_scale)
+            .setView(view)
+            .setNegativeButton(android.R.string.cancel, null)
+            .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
+                setControlScale(slider.value.toInt())
+            }
+            .setNeutralButton(R.string.slider_default) { _: DialogInterface?, _: Int ->
+                setControlScale(50)
+            }
+            .show()
+    }
+
+    private fun setControlScale(scale: Int) {
+        PreferenceManager.getDefaultSharedPreferences(applicationContext).edit()
+            .putInt(Settings.PREF_CONTROL_SCALE, scale)
+            .apply()
+        emulationFragment!!.refreshInputOverlay()
+    }
+
+    private fun resetOverlay() {
+        MaterialAlertDialogBuilder(this)
+            .setTitle(getString(R.string.emulation_touch_overlay_reset))
+            .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> emulationFragment!!.resetInputOverlay() }
+            .setNegativeButton(android.R.string.cancel, null)
+            .create()
+            .show()
+    }
+
+    override fun dispatchTouchEvent(event: MotionEvent): Boolean {
+        if (event.actionMasked == MotionEvent.ACTION_DOWN) {
+            var anyMenuClosed = false
+            var submenu = supportFragmentManager.findFragmentById(R.id.frame_submenu)
+            if (submenu != null && areCoordinatesOutside(submenu.view, event.x, event.y)) {
+                closeSubmenu()
+                submenu = null
+                anyMenuClosed = true
+            }
+            if (submenu == null) {
+                val menu = supportFragmentManager.findFragmentById(R.id.frame_menu)
+                if (menu != null && areCoordinatesOutside(menu.view, event.x, event.y)) {
+                    closeMenu()
+                    anyMenuClosed = true
+                }
+            }
+            if (anyMenuClosed) {
+                return true
+            }
+        }
+        return super.dispatchTouchEvent(event)
+    }
+
+    @Retention(AnnotationRetention.SOURCE)
+    @IntDef(
+        MENU_ACTION_EDIT_CONTROLS_PLACEMENT,
+        MENU_ACTION_TOGGLE_CONTROLS,
+        MENU_ACTION_ADJUST_SCALE,
+        MENU_ACTION_EXIT,
+        MENU_ACTION_SHOW_FPS,
+        MENU_ACTION_RESET_OVERLAY,
+        MENU_ACTION_SHOW_OVERLAY,
+        MENU_ACTION_OPEN_SETTINGS
+    )
+    annotation class MenuAction
+
+    private fun closeSubmenu(): Boolean {
+        return supportFragmentManager.popBackStackImmediate(
+            BACKSTACK_NAME_SUBMENU,
+            FragmentManager.POP_BACK_STACK_INCLUSIVE
+        )
+    }
+
+    private fun closeMenu(): Boolean {
+        menuVisible = false
+        return supportFragmentManager.popBackStackImmediate(
+            BACKSTACK_NAME_MENU,
+            FragmentManager.POP_BACK_STACK_INCLUSIVE
+        )
+    }
+
+    private fun toggleMenu() {
+        if (!closeMenu()) {
+            val fragment: Fragment = MenuFragment.newInstance()
+            supportFragmentManager.beginTransaction()
+                .setCustomAnimations(
+                    R.animator.menu_slide_in_from_start,
+                    R.animator.menu_slide_out_to_start,
+                    R.animator.menu_slide_in_from_start,
+                    R.animator.menu_slide_out_to_start
+                )
+                .add(R.id.frame_menu, fragment)
+                .addToBackStack(BACKSTACK_NAME_MENU)
+                .commit()
+            menuVisible = true
+        }
+    }
+
+    companion object {
+        private const val BACKSTACK_NAME_MENU = "menu"
+        private const val BACKSTACK_NAME_SUBMENU = "submenu"
+        const val EXTRA_SELECTED_GAME = "SelectedGame"
+        const val EXTRA_SELECTED_TITLE = "SelectedTitle"
+        const val MENU_ACTION_EDIT_CONTROLS_PLACEMENT = 0
+        const val MENU_ACTION_TOGGLE_CONTROLS = 1
+        const val MENU_ACTION_ADJUST_SCALE = 2
+        const val MENU_ACTION_EXIT = 3
+        const val MENU_ACTION_SHOW_FPS = 4
+        const val MENU_ACTION_RESET_OVERLAY = 6
+        const val MENU_ACTION_SHOW_OVERLAY = 7
+        const val MENU_ACTION_OPEN_SETTINGS = 8
+        private const val EMULATION_RUNNING_NOTIFICATION = 0x1000
+
+        @JvmStatic
+        fun launch(activity: FragmentActivity, path: String?, title: String?) {
+            val launcher = Intent(activity, EmulationActivity::class.java)
+            launcher.putExtra(EXTRA_SELECTED_GAME, path)
+            launcher.putExtra(EXTRA_SELECTED_TITLE, title)
+            activity.startActivity(launcher)
+        }
+
+        @JvmStatic
+        fun tryDismissRunningNotification(activity: Activity?) {
+            // TODO(bunnei): Disable notifications until we support app suspension.
+            //NotificationManagerCompat.from(activity).cancel(EMULATION_RUNNING_NOTIFICATION);
+        }
+
+        private fun areCoordinatesOutside(view: View?, x: Float, y: Float): Boolean {
+            if (view == null) {
+                return true
+            }
+            val viewBounds = Rect()
+            view.getGlobalVisibleRect(viewBounds)
+            return !viewBounds.contains(x.roundToInt(), y.roundToInt())
+        }
+    }
+}