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 deleted file mode 100644 index 3a7c9c1067..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.java +++ /dev/null @@ -1,355 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.fragments; - -import android.content.Context; -import android.graphics.Rect; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.SurfaceHolder; -import android.view.SurfaceView; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; - -import org.dolphinemu.dolphinemu.NativeLibrary; -import org.dolphinemu.dolphinemu.activities.EmulationActivity; -import org.dolphinemu.dolphinemu.databinding.FragmentEmulationBinding; -import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting; -import org.dolphinemu.dolphinemu.features.settings.model.Settings; -import org.dolphinemu.dolphinemu.overlay.InputOverlay; -import org.dolphinemu.dolphinemu.utils.Log; - -import java.io.File; - -public final class EmulationFragment extends Fragment implements SurfaceHolder.Callback -{ - private static final String KEY_GAMEPATHS = "gamepaths"; - private static final String KEY_RIIVOLUTION = "riivolution"; - private static final String KEY_SYSTEM_MENU = "systemMenu"; - - private InputOverlay mInputOverlay; - - private String[] mGamePaths; - private boolean mRiivolution; - private boolean mRunWhenSurfaceIsValid; - private boolean mLoadPreviousTemporaryState; - private boolean mLaunchSystemMenu; - - private EmulationActivity activity; - - private FragmentEmulationBinding mBinding; - - public static EmulationFragment newInstance(String[] gamePaths, boolean riivolution, - boolean systemMenu) - { - Bundle args = new Bundle(); - args.putStringArray(KEY_GAMEPATHS, gamePaths); - args.putBoolean(KEY_RIIVOLUTION, riivolution); - args.putBoolean(KEY_SYSTEM_MENU, systemMenu); - - EmulationFragment fragment = new EmulationFragment(); - fragment.setArguments(args); - return fragment; - } - - @Override - public void onAttach(@NonNull Context context) - { - super.onAttach(context); - - if (context instanceof EmulationActivity) - { - activity = (EmulationActivity) context; - NativeLibrary.setEmulationActivity((EmulationActivity) context); - } - else - { - throw new IllegalStateException("EmulationFragment must have EmulationActivity parent"); - } - } - - /** - * Initialize anything that doesn't depend on the layout / views in here. - */ - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - - mGamePaths = getArguments().getStringArray(KEY_GAMEPATHS); - mRiivolution = getArguments().getBoolean(KEY_RIIVOLUTION); - mLaunchSystemMenu = getArguments().getBoolean(KEY_SYSTEM_MENU); - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) - { - mBinding = FragmentEmulationBinding.inflate(inflater, container, false); - return mBinding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) - { - // The new Surface created here will get passed to the native code via onSurfaceChanged. - SurfaceView surfaceView = mBinding.surfaceEmulation; - surfaceView.getHolder().addCallback(this); - - mInputOverlay = mBinding.surfaceInputOverlay; - - Button doneButton = mBinding.doneControlConfig; - if (doneButton != null) - { - doneButton.setOnClickListener(v -> stopConfiguringControls()); - } - - if (mInputOverlay != null) - { - view.post(() -> - { - int overlayX = mInputOverlay.getLeft(); - int overlayY = mInputOverlay.getTop(); - mInputOverlay.setSurfacePosition(new Rect( - surfaceView.getLeft() - overlayX, surfaceView.getTop() - overlayY, - surfaceView.getRight() - overlayX, surfaceView.getBottom() - overlayY)); - }); - } - } - - @Override - public void onDestroyView() - { - super.onDestroyView(); - mBinding = null; - } - - @Override - public void onResume() - { - super.onResume(); - - if (mInputOverlay != null && NativeLibrary.IsGameMetadataValid()) - mInputOverlay.refreshControls(); - - run(activity.isActivityRecreated()); - } - - @Override - public void onPause() - { - if (NativeLibrary.IsRunningAndUnpaused() && !NativeLibrary.IsShowingAlertMessage()) - { - Log.debug("[EmulationFragment] Pausing emulation."); - NativeLibrary.PauseEmulation(); - } - - super.onPause(); - } - - @Override - public void onDestroy() - { - if (mInputOverlay != null) - mInputOverlay.onDestroy(); - - super.onDestroy(); - } - - @Override - public void onDetach() - { - NativeLibrary.clearEmulationActivity(); - super.onDetach(); - } - - public void toggleInputOverlayVisibility(Settings settings) - { - BooleanSetting.MAIN_SHOW_INPUT_OVERLAY - .setBoolean(settings, !BooleanSetting.MAIN_SHOW_INPUT_OVERLAY.getBoolean()); - - if (mInputOverlay != null) - mInputOverlay.refreshControls(); - } - - public void initInputPointer() - { - if (mInputOverlay != null) - mInputOverlay.initTouchPointer(); - } - - public void refreshInputOverlay() - { - if (mInputOverlay != null) - mInputOverlay.refreshControls(); - } - - public void refreshOverlayPointer() - { - if (mInputOverlay != null) - mInputOverlay.refreshOverlayPointer(); - } - - public void resetInputOverlay() - { - if (mInputOverlay != null) - mInputOverlay.resetButtonPlacement(); - } - - @Override - public void surfaceCreated(@NonNull SurfaceHolder holder) - { - // We purposely don't do anything here. - // All work is done in surfaceChanged, which we are guaranteed to get even for surface creation. - } - - @Override - public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) - { - Log.debug("[EmulationFragment] Surface changed. Resolution: " + width + "x" + height); - NativeLibrary.SurfaceChanged(holder.getSurface()); - if (mRunWhenSurfaceIsValid) - { - runWithValidSurface(); - } - } - - @Override - public void surfaceDestroyed(@NonNull SurfaceHolder holder) - { - Log.debug("[EmulationFragment] Surface destroyed."); - NativeLibrary.SurfaceDestroyed(); - mRunWhenSurfaceIsValid = true; - } - - public void stopEmulation() - { - Log.debug("[EmulationFragment] Stopping emulation."); - NativeLibrary.StopEmulation(); - } - - public void startConfiguringControls() - { - if (mInputOverlay != null) - { - mBinding.doneControlConfig.setVisibility(View.VISIBLE); - mInputOverlay.setEditMode(true); - } - } - - public void stopConfiguringControls() - { - if (mInputOverlay != null) - { - mBinding.doneControlConfig.setVisibility(View.GONE); - mInputOverlay.setEditMode(false); - } - } - - public boolean isConfiguringControls() - { - return mInputOverlay != null && mInputOverlay.isInEditMode(); - } - - private void run(boolean isActivityRecreated) - { - if (isActivityRecreated) - { - if (NativeLibrary.IsRunning()) - { - mLoadPreviousTemporaryState = false; - deleteFile(getTemporaryStateFilePath()); - } - else - { - mLoadPreviousTemporaryState = true; - } - } - else - { - Log.debug("[EmulationFragment] activity resumed or fresh start"); - mLoadPreviousTemporaryState = false; - // activity resumed without being killed or this is the first run - deleteFile(getTemporaryStateFilePath()); - } - - // If the surface is set, run now. Otherwise, wait for it to get set. - if (NativeLibrary.HasSurface()) - { - runWithValidSurface(); - } - else - { - mRunWhenSurfaceIsValid = true; - } - } - - private void runWithValidSurface() - { - mRunWhenSurfaceIsValid = false; - if (!NativeLibrary.IsRunning()) - { - NativeLibrary.SetIsBooting(); - - Thread emulationThread = new Thread(() -> - { - if (mLoadPreviousTemporaryState) - { - Log.debug("[EmulationFragment] Starting emulation thread from previous state."); - NativeLibrary.Run(mGamePaths, mRiivolution, getTemporaryStateFilePath(), true); - } - if (mLaunchSystemMenu) - { - Log.debug("[EmulationFragment] Starting emulation thread for the Wii Menu."); - NativeLibrary.RunSystemMenu(); - } - else - { - Log.debug("[EmulationFragment] Starting emulation thread."); - NativeLibrary.Run(mGamePaths, mRiivolution); - } - EmulationActivity.stopIgnoringLaunchRequests(); - }, "NativeEmulation"); - emulationThread.start(); - } - else - { - if (!EmulationActivity.Companion.getHasUserPausedEmulation() && - !NativeLibrary.IsShowingAlertMessage()) - { - Log.debug("[EmulationFragment] Resuming emulation."); - NativeLibrary.UnPauseEmulation(); - } - } - } - - public void saveTemporaryState() - { - NativeLibrary.SaveStateAs(getTemporaryStateFilePath(), true); - } - - private String getTemporaryStateFilePath() - { - return getContext().getFilesDir() + File.separator + "temp.sav"; - } - - private static void deleteFile(String path) - { - try - { - File file = new File(path); - if (!file.delete()) - { - Log.error("[EmulationFragment] Failed to delete " + file.getAbsolutePath()); - } - } - catch (Exception ignored) - { - } - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt new file mode 100644 index 0000000000..e403e7ef5d --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.fragments + +import android.content.Context +import android.graphics.Rect +import android.os.Bundle +import android.view.LayoutInflater +import android.view.SurfaceHolder +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import org.dolphinemu.dolphinemu.NativeLibrary +import org.dolphinemu.dolphinemu.activities.EmulationActivity +import org.dolphinemu.dolphinemu.databinding.FragmentEmulationBinding +import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting +import org.dolphinemu.dolphinemu.features.settings.model.Settings +import org.dolphinemu.dolphinemu.overlay.InputOverlay +import org.dolphinemu.dolphinemu.utils.Log +import java.io.File + +class EmulationFragment : Fragment(), SurfaceHolder.Callback { + private var inputOverlay: InputOverlay? = null + + private var gamePaths: Array? = null + private var riivolution = false + private var runWhenSurfaceIsValid = false + private var loadPreviousTemporaryState = false + private var launchSystemMenu = false + + private var emulationActivity: EmulationActivity? = null + + private var _binding: FragmentEmulationBinding? = null + private val binding get() = _binding!! + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context is EmulationActivity) { + emulationActivity = context + NativeLibrary.setEmulationActivity(context) + } else { + throw IllegalStateException("EmulationFragment must have EmulationActivity parent") + } + } + + /** + * Initialize anything that doesn't depend on the layout / views in here. + */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + requireArguments().apply { + gamePaths = getStringArray(KEY_GAMEPATHS) + riivolution = getBoolean(KEY_RIIVOLUTION) + launchSystemMenu = getBoolean(KEY_SYSTEM_MENU) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentEmulationBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + // The new Surface created here will get passed to the native code via onSurfaceChanged. + val surfaceView = binding.surfaceEmulation + surfaceView.holder.addCallback(this) + + inputOverlay = binding.surfaceInputOverlay + + val doneButton = binding.doneControlConfig + doneButton?.setOnClickListener { stopConfiguringControls() } + + if (inputOverlay != null) { + view.post { + val overlayX = inputOverlay!!.left + val overlayY = inputOverlay!!.top + inputOverlay?.setSurfacePosition( + Rect( + surfaceView.left - overlayX, + surfaceView.top - overlayY, + surfaceView.right - overlayX, + surfaceView.bottom - overlayY + ) + ) + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + override fun onResume() { + super.onResume() + if (NativeLibrary.IsGameMetadataValid()) + inputOverlay?.refreshControls() + + run(emulationActivity!!.isActivityRecreated) + } + + override fun onPause() { + if (NativeLibrary.IsRunningAndUnpaused() && !NativeLibrary.IsShowingAlertMessage()) { + Log.debug("[EmulationFragment] Pausing emulation.") + NativeLibrary.PauseEmulation() + } + super.onPause() + } + + override fun onDestroy() { + inputOverlay?.onDestroy() + super.onDestroy() + } + + override fun onDetach() { + NativeLibrary.clearEmulationActivity() + super.onDetach() + } + + fun toggleInputOverlayVisibility(settings: Settings?) { + BooleanSetting.MAIN_SHOW_INPUT_OVERLAY.setBoolean( + settings!!, + !BooleanSetting.MAIN_SHOW_INPUT_OVERLAY.boolean + ) + + inputOverlay?.refreshControls() + } + + fun initInputPointer() = inputOverlay?.initTouchPointer() + + fun refreshInputOverlay() = inputOverlay?.refreshControls() + + fun refreshOverlayPointer() = inputOverlay?.refreshOverlayPointer() + + fun resetInputOverlay() = inputOverlay?.resetButtonPlacement() + + override fun surfaceCreated(holder: SurfaceHolder) { + // We purposely don't do anything here. + // All work is done in surfaceChanged, which we are guaranteed to get even for surface creation. + } + + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { + Log.debug("[EmulationFragment] Surface changed. Resolution: $width x $height") + NativeLibrary.SurfaceChanged(holder.surface) + if (runWhenSurfaceIsValid) { + runWithValidSurface() + } + } + + override fun surfaceDestroyed(holder: SurfaceHolder) { + Log.debug("[EmulationFragment] Surface destroyed.") + NativeLibrary.SurfaceDestroyed() + runWhenSurfaceIsValid = true + } + + fun stopEmulation() { + Log.debug("[EmulationFragment] Stopping emulation.") + NativeLibrary.StopEmulation() + } + + fun startConfiguringControls() { + binding.doneControlConfig?.visibility = View.VISIBLE + inputOverlay?.editMode = true + } + + fun stopConfiguringControls() { + binding.doneControlConfig?.visibility = View.GONE + inputOverlay?.editMode = false + } + + val isConfiguringControls: Boolean + get() = inputOverlay != null && inputOverlay!!.isInEditMode + + private fun run(isActivityRecreated: Boolean) { + if (isActivityRecreated) { + if (NativeLibrary.IsRunning()) { + loadPreviousTemporaryState = false + deleteFile(temporaryStateFilePath) + } else { + loadPreviousTemporaryState = true + } + } else { + Log.debug("[EmulationFragment] activity resumed or fresh start") + loadPreviousTemporaryState = false + // activity resumed without being killed or this is the first run + deleteFile(temporaryStateFilePath) + } + + // If the surface is set, run now. Otherwise, wait for it to get set. + if (NativeLibrary.HasSurface()) { + runWithValidSurface() + } else { + runWhenSurfaceIsValid = true + } + } + + private fun runWithValidSurface() { + runWhenSurfaceIsValid = false + if (!NativeLibrary.IsRunning()) { + NativeLibrary.SetIsBooting() + val emulationThread = Thread({ + if (loadPreviousTemporaryState) { + Log.debug("[EmulationFragment] Starting emulation thread from previous state.") + NativeLibrary.Run(gamePaths, riivolution, temporaryStateFilePath, true) + } + if (launchSystemMenu) { + Log.debug("[EmulationFragment] Starting emulation thread for the Wii Menu.") + NativeLibrary.RunSystemMenu() + } else { + Log.debug("[EmulationFragment] Starting emulation thread.") + NativeLibrary.Run(gamePaths, riivolution) + } + EmulationActivity.stopIgnoringLaunchRequests() + }, "NativeEmulation") + emulationThread.start() + } else { + if (!EmulationActivity.hasUserPausedEmulation && !NativeLibrary.IsShowingAlertMessage()) { + Log.debug("[EmulationFragment] Resuming emulation.") + NativeLibrary.UnPauseEmulation() + } + } + } + + fun saveTemporaryState() = NativeLibrary.SaveStateAs(temporaryStateFilePath, true) + + private val temporaryStateFilePath: String + get() = "${requireContext().filesDir}${File.separator}temp.sav" + + companion object { + private const val KEY_GAMEPATHS = "gamepaths" + private const val KEY_RIIVOLUTION = "riivolution" + private const val KEY_SYSTEM_MENU = "systemMenu" + + fun newInstance( + gamePaths: Array?, + riivolution: Boolean, + systemMenu: Boolean + ): EmulationFragment { + val args = Bundle() + args.apply { + putStringArray(KEY_GAMEPATHS, gamePaths) + putBoolean(KEY_RIIVOLUTION, riivolution) + putBoolean(KEY_SYSTEM_MENU, systemMenu) + } + val fragment = EmulationFragment() + fragment.arguments = args + return fragment + } + + private fun deleteFile(path: String) { + try { + val file = File(path) + if (!file.delete()) { + Log.error("[EmulationFragment] Failed to delete ${file.absolutePath}") + } + } catch (ignored: Exception) { + } + } + } +}