diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index 34166107..d83049f8 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml @@ -9,10 +9,10 @@ + tools:ignore="DiscouragedApi"> + + + + + + + + + + + + + + + + + NewStringUTF(game.name.c_str()); - jlong titleId = static_cast(game.titleId); + jstring path = game.path.has_value() ? env->NewStringUTF(game.path->c_str()) : nullptr; int width = -1, height = -1; jintArray jIconData = nullptr; if (icon) @@ -27,9 +27,11 @@ class AndroidGameTitleLoadedCallback : public GameTitleLoadedCallback jIconData = env->NewIntArray(width * height); env->SetIntArrayRegion(jIconData, 0, width * height, reinterpret_cast(icon->intColors())); } - env->CallVoidMethod(*m_gameTitleLoadedCallbackObj, m_onGameTitleLoadedMID, static_cast(titleId), name, jIconData, width, height); + env->CallVoidMethod(*m_gameTitleLoadedCallbackObj, m_onGameTitleLoadedMID, path, name, jIconData, width, height); if (jIconData != nullptr) env->DeleteLocalRef(jIconData); + if (path != nullptr) + env->DeleteLocalRef(path); env->DeleteLocalRef(name); } }; diff --git a/src/android/app/src/main/cpp/CafeSystemUtils.cpp b/src/android/app/src/main/cpp/CafeSystemUtils.cpp index cbf7fcbe..4c4b55c7 100644 --- a/src/android/app/src/main/cpp/CafeSystemUtils.cpp +++ b/src/android/app/src/main/cpp/CafeSystemUtils.cpp @@ -5,47 +5,50 @@ namespace CafeSystemUtils { - void startGame(TitleId titleId) + void startGame(const fs::path& launchPath) { - TitleInfo launchTitle; - - if (!CafeTitleList::GetFirstByTitleId(titleId, launchTitle)) - return; - + TitleInfo launchTitle{launchPath}; if (launchTitle.IsValid()) { // the title might not be in the TitleList, so we add it as a temporary entry - CafeTitleList::AddTitleFromPath(launchTitle.GetPath()); + CafeTitleList::AddTitleFromPath(launchPath); // title is valid, launch from TitleId TitleId baseTitleId; if (!CafeTitleList::FindBaseTitleId(launchTitle.GetAppTitleId(), baseTitleId)) { + throw GameBaseFilesNotFoundException(); } - CafeSystem::STATUS_CODE statusCode = CafeSystem::PrepareForegroundTitle(baseTitleId); - if (statusCode == CafeSystem::STATUS_CODE::INVALID_RPX) - { - } - else if (statusCode == CafeSystem::STATUS_CODE::UNABLE_TO_MOUNT) - { - } - else if (statusCode != CafeSystem::STATUS_CODE::SUCCESS) + CafeSystem::STATUS_CODE r = CafeSystem::PrepareForegroundTitle(baseTitleId); + if (r != CafeSystem::STATUS_CODE::SUCCESS) { + throw UnknownGameFilesException(); } } else // if (launchTitle.GetFormat() == TitleInfo::TitleDataFormat::INVALID_STRUCTURE ) { - // title is invalid, if its an RPX/ELF we can launch it directly - // otherwise its an error - CafeTitleFileType fileType = DetermineCafeSystemFileType(launchTitle.GetPath()); + // title is invalid, if it's an RPX/ELF we can launch it directly + // otherwise it's an error + CafeTitleFileType fileType = DetermineCafeSystemFileType(launchPath); if (fileType == CafeTitleFileType::RPX || fileType == CafeTitleFileType::ELF) { - CafeSystem::STATUS_CODE statusCode = CafeSystem::PrepareForegroundTitleFromStandaloneRPX( - launchTitle.GetPath()); - if (statusCode != CafeSystem::STATUS_CODE::SUCCESS) + CafeSystem::STATUS_CODE r = CafeSystem::PrepareForegroundTitleFromStandaloneRPX(launchPath); + if (r != CafeSystem::STATUS_CODE::SUCCESS) { - return; + throw UnknownGameFilesException(); } } + else if (launchTitle.GetInvalidReason() == TitleInfo::InvalidReason::NO_DISC_KEY) + { + throw NoDiscKeyException(); + } + else if (launchTitle.GetInvalidReason() == TitleInfo::InvalidReason::NO_TITLE_TIK) + { + throw NoTitleTikException(); + } + else + { + throw UnknownGameFilesException(); + } } CafeSystem::LaunchForegroundTitle(); } diff --git a/src/android/app/src/main/cpp/CafeSystemUtils.h b/src/android/app/src/main/cpp/CafeSystemUtils.h index 99d8d3e0..8cdaed43 100644 --- a/src/android/app/src/main/cpp/CafeSystemUtils.h +++ b/src/android/app/src/main/cpp/CafeSystemUtils.h @@ -4,5 +4,48 @@ namespace CafeSystemUtils { - void startGame(TitleId titleId); -}; \ No newline at end of file + class GameFilesException : public std::exception + { + public: + explicit GameFilesException(const std::string& message) + : m_message(message) {} + + const char* what() const noexcept override + { + return m_message.c_str(); + } + + private: + std::string m_message; + }; + + class GameBaseFilesNotFoundException : public GameFilesException + { + public: + GameBaseFilesNotFoundException() + : GameFilesException("Game base files not found.") {} + }; + + class NoDiscKeyException : public GameFilesException + { + public: + NoDiscKeyException() + : GameFilesException("No disc key found.") {} + }; + + class NoTitleTikException : public GameFilesException + { + public: + NoTitleTikException() + : GameFilesException("No title ticket found.") {} + }; + + class UnknownGameFilesException : public GameFilesException + { + public: + UnknownGameFilesException() + : GameFilesException("Unknown error occurred during game launch.") {} + }; + + void startGame(const fs::path& launchPath); +}; // namespace CafeSystemUtils \ No newline at end of file diff --git a/src/android/app/src/main/cpp/EmulationState.h b/src/android/app/src/main/cpp/EmulationState.h index 3bdebc1e..91815538 100644 --- a/src/android/app/src/main/cpp/EmulationState.h +++ b/src/android/app/src/main/cpp/EmulationState.h @@ -268,11 +268,11 @@ class EmulationState m_gameTitleLoader.reloadGameTitles(); } - void startGame(TitleId titleId) + void startGame(const fs::path& gamePath) { GuiSystem::getWindowInfo().set_keystates_up(); initializeAudioDevices(); - CafeSystemUtils::startGame(titleId); + CafeSystemUtils::startGame(gamePath); } void refreshGraphicPacks() diff --git a/src/android/app/src/main/cpp/GameTitleLoader.cpp b/src/android/app/src/main/cpp/GameTitleLoader.cpp index 38ba545b..884ed04e 100644 --- a/src/android/app/src/main/cpp/GameTitleLoader.cpp +++ b/src/android/app/src/main/cpp/GameTitleLoader.cpp @@ -72,6 +72,8 @@ void GameTitleLoader::titleRefresh(TitleId titleId) Game& game = m_gameInfos[baseTitleId]; std::optional titleInfo = getFirstTitleInfoByTitleId(titleId); game.titleId = baseTitleId; + if (titleInfo.has_value()) + game.path = titleInfo->GetPath(); game.name = getNameByTitleId(baseTitleId, titleInfo); game.version = gameInfo.GetVersion(); game.region = gameInfo.GetRegion(); diff --git a/src/android/app/src/main/cpp/GameTitleLoader.h b/src/android/app/src/main/cpp/GameTitleLoader.h index 21709bb2..9cbb49df 100644 --- a/src/android/app/src/main/cpp/GameTitleLoader.h +++ b/src/android/app/src/main/cpp/GameTitleLoader.h @@ -8,6 +8,7 @@ struct Game { std::string name; + std::optional path; uint32 secondsPlayed; uint16 dlc; uint16 version; @@ -41,7 +42,6 @@ class GameTitleLoader void reloadGameTitles(); ~GameTitleLoader(); void titleRefresh(TitleId titleId); - void addGamePath(const fs::path& path); private: void loadGameTitles(); diff --git a/src/android/app/src/main/cpp/native-lib.cpp b/src/android/app/src/main/cpp/native-lib.cpp index 312d9fef..8b69978e 100644 --- a/src/android/app/src/main/cpp/native-lib.cpp +++ b/src/android/app/src/main/cpp/native-lib.cpp @@ -26,9 +26,29 @@ Java_info_cemu_Cemu_NativeLibrary_setSurfaceSize([[maybe_unused]] JNIEnv* env, [ } extern "C" [[maybe_unused]] JNIEXPORT void JNICALL -Java_info_cemu_Cemu_NativeLibrary_startGame([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jlong title_id) +Java_info_cemu_Cemu_NativeLibrary_startGame([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jstring launchPath) { - s_emulationState.startGame(static_cast(title_id)); + using namespace CafeSystemUtils; + try + { + s_emulationState.startGame(JNIUtils::JStringToString(env, launchPath)); + } catch (const GameBaseFilesNotFoundException& exception) + { + jclass exceptionClass = env->FindClass("info/cemu/Cemu/NativeLibrary$GameBaseFilesNotFoundException"); + env->ThrowNew(exceptionClass, exception.what()); + } catch (const NoDiscKeyException& exception) + { + jclass exceptionClass = env->FindClass("info/cemu/Cemu/NativeLibrary$NoDiscKeyException"); + env->ThrowNew(exceptionClass, exception.what()); + } catch (const NoTitleTikException& exception) + { + jclass exceptionClass = env->FindClass("info/cemu/Cemu/NativeLibrary$NoTitleTikException"); + env->ThrowNew(exceptionClass, exception.what()); + } catch (const std::exception& exception) + { + jclass exceptionClass = env->FindClass("info/cemu/Cemu/NativeLibrary$UnknownGameFilesException"); + env->ThrowNew(exceptionClass, exception.what()); + } } extern "C" [[maybe_unused]] JNIEXPORT void JNICALL @@ -40,7 +60,7 @@ Java_info_cemu_Cemu_NativeLibrary_setGameTitleLoadedCallback(JNIEnv* env, [[mayb return; } jclass gameTitleLoadedCallbackClass = env->GetObjectClass(game_title_loaded_callback); - jmethodID onGameTitleLoadedMID = env->GetMethodID(gameTitleLoadedCallbackClass, "onGameTitleLoaded", "(JLjava/lang/String;[III)V"); + jmethodID onGameTitleLoadedMID = env->GetMethodID(gameTitleLoadedCallbackClass, "onGameTitleLoaded", "(Ljava/lang/String;Ljava/lang/String;[III)V"); env->DeleteLocalRef(gameTitleLoadedCallbackClass); s_emulationState.setOnGameTitleLoaded(std::make_shared(onGameTitleLoadedMID, game_title_loaded_callback)); } diff --git a/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java b/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java index b00204db..8b02da36 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java @@ -22,14 +22,29 @@ public class NativeLibrary { public static native void initializerRenderer(Surface surface); - public static native void startGame(long titleId); + public static class GameFilesException extends RuntimeException { + } + + public static class GameBaseFilesNotFoundException extends GameFilesException { + } + + public static class NoDiscKeyException extends GameFilesException { + } + + public static class NoTitleTikException extends GameFilesException { + } + + public static class UnknownGameFilesException extends GameFilesException { + } + + public static native void startGame(String launchPath); public static native void setReplaceTVWithPadView(boolean swapped); public static native void recreateRenderSurface(boolean isMainCanvas); public interface GameTitleLoadedCallback { - void onGameTitleLoaded(long titleId, String title, int[] colors, int width, int height); + void onGameTitleLoaded(String path, String title, int[] colors, int width, int height); } public static native void setGameTitleLoadedCallback(GameTitleLoadedCallback gameTitleLoadedCallback); diff --git a/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationActivity.java b/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationActivity.java index a696cb57..b23794ac 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationActivity.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationActivity.java @@ -1,5 +1,8 @@ package info.cemu.Cemu.emulation; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import android.view.KeyEvent; import android.view.MotionEvent; @@ -8,6 +11,8 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.core.view.WindowCompat; import androidx.core.view.WindowInsetsCompat; import androidx.core.view.WindowInsetsControllerCompat; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; import com.google.android.material.dialog.MaterialAlertDialogBuilder; @@ -15,10 +20,10 @@ import info.cemu.Cemu.R; import info.cemu.Cemu.databinding.ActivityEmulationBinding; import info.cemu.Cemu.input.InputManager; -public class EmulationActivity extends AppCompatActivity { - - public static final String GAME_TITLE_ID = "GameTitleId"; - private long gameTitleId; +public class EmulationActivity extends AppCompatActivity implements Observer { + private EmulationViewModel viewModel; + private boolean hasEmulationError; + public static final String LAUNCH_PATH = "LAUNCH_PATH"; private ActivityEmulationBinding binding; private EmulationFragment emulationFragment; private final InputManager inputManager = new InputManager(); @@ -40,16 +45,26 @@ public class EmulationActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - Bundle extras = getIntent().getExtras(); + viewModel = new ViewModelProvider(this).get(EmulationViewModel.class); + viewModel.getEmulationData().observe(this, this); + Intent intent = getIntent(); + Bundle extras = intent.getExtras(); + Uri data = intent.getData(); + String launchPath = null; if (extras != null) { - gameTitleId = extras.getLong(GAME_TITLE_ID); + launchPath = extras.getString(LAUNCH_PATH); + } else if (data != null) { + launchPath = data.toString(); + } + if (launchPath == null) { + throw new RuntimeException("launchPath is null"); } setFullscreen(); binding = ActivityEmulationBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); emulationFragment = (EmulationFragment) getSupportFragmentManager().findFragmentById(R.id.emulation_frame); if (emulationFragment == null) { - emulationFragment = new EmulationFragment(gameTitleId); + emulationFragment = new EmulationFragment(launchPath); getSupportFragmentManager() .beginTransaction() .add(R.id.emulation_frame, emulationFragment) @@ -66,10 +81,18 @@ public class EmulationActivity extends AppCompatActivity { MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setTitle(R.string.exit_confirmation_title) .setMessage(R.string.exit_confirm_message) - .setPositiveButton(R.string.yes, (dialog, which) -> { - finishAffinity(); - System.exit(0); - }).setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel()) + .setPositiveButton(R.string.yes, (dialog, which) -> quit()) + .setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel()) + .create() + .show(); + } + + private void onEmulationError(EmulationError emulationError) { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); + builder.setTitle(R.string.error) + .setMessage(emulationError.errorMessage()) + .setNeutralButton(R.string.quit, (dialog, which) -> dialog.dismiss()) + .setOnDismissListener(dialog -> quit()) .create() .show(); } @@ -80,4 +103,17 @@ public class EmulationActivity extends AppCompatActivity { controller.hide(WindowInsetsCompat.Type.systemBars()); controller.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); } + + private void quit() { + finishAffinity(); + System.exit(0); + } + + @Override + public void onChanged(EmulationData emulationData) { + if (emulationData.emulationError().isPresent() && !hasEmulationError) { + hasEmulationError = true; + onEmulationError(emulationData.emulationError().get()); + } + } } \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationData.java b/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationData.java new file mode 100644 index 00000000..b77bd791 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationData.java @@ -0,0 +1,6 @@ +package info.cemu.Cemu.emulation; + +import java.util.Optional; + +public record EmulationData(Optional emulationError) { +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationError.java b/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationError.java new file mode 100644 index 00000000..d7c5f14f --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationError.java @@ -0,0 +1,4 @@ +package info.cemu.Cemu.emulation; + +public record EmulationError(String errorMessage) { +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationFragment.java b/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationFragment.java index 54d2a705..29657d4a 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationFragment.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationFragment.java @@ -1,5 +1,7 @@ package info.cemu.Cemu.emulation; +import static androidx.core.app.ActivityCompat.finishAffinity; + import android.annotation.SuppressLint; import android.content.res.Configuration; import android.graphics.SurfaceTexture; @@ -20,6 +22,9 @@ import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.appcompat.widget.PopupMenu; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import info.cemu.Cemu.NativeLibrary; import info.cemu.Cemu.R; @@ -53,6 +58,7 @@ public class EmulationFragment extends Fragment implements PopupMenu.OnMenuItemC return true; } case MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> { + currentPointerId = -1; NativeLibrary.onTouchUp(x, y, isTV); return true; } @@ -80,10 +86,11 @@ public class EmulationFragment extends Fragment implements PopupMenu.OnMenuItemC @Override public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int format, int width, int height) { NativeLibrary.setSurfaceSize(width, height, isMainCanvas); - if (!surfaceSet) { - surfaceSet = true; - NativeLibrary.setSurface(surfaceHolder.getSurface(), isMainCanvas); + if (surfaceSet) { + return; } + surfaceSet = true; + NativeLibrary.setSurface(surfaceHolder.getSurface(), isMainCanvas); } @Override @@ -93,7 +100,7 @@ public class EmulationFragment extends Fragment implements PopupMenu.OnMenuItemC } } - private final long gameTitleId; + private final String launchPath; private boolean isGameRunning; private SurfaceView padCanvas; private SurfaceTexture testSurfaceTexture; @@ -104,9 +111,10 @@ public class EmulationFragment extends Fragment implements PopupMenu.OnMenuItemC private PopupMenu settingsMenu; private InputOverlaySurfaceView inputOverlaySurfaceView; private SensorManager sensorManager; + private EmulationViewModel viewModel; - public EmulationFragment(long gameTitleId) { - this.gameTitleId = gameTitleId; + public EmulationFragment(String launchPath) { + this.launchPath = launchPath; } InputOverlaySettingsProvider.OverlaySettings overlaySettings; @@ -221,6 +229,12 @@ public class EmulationFragment extends Fragment implements PopupMenu.OnMenuItemC return false; } + @Override + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + viewModel = new ViewModelProvider(requireActivity()).get(EmulationViewModel.class); + } + @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentEmulationBinding.inflate(inflater, container, false); @@ -271,7 +285,7 @@ public class EmulationFragment extends Fragment implements PopupMenu.OnMenuItemC public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) { if (!isGameRunning) { isGameRunning = true; - NativeLibrary.startGame(gameTitleId); + startGame(); } } @@ -282,4 +296,24 @@ public class EmulationFragment extends Fragment implements PopupMenu.OnMenuItemC mainCanvas.setOnTouchListener(new OnSurfaceTouchListener(true)); return binding.getRoot(); } + + private void startGame() { + try { + NativeLibrary.startGame(launchPath); + } catch (NativeLibrary.GameBaseFilesNotFoundException exception) { + onEmulationError(getString(R.string.game_not_found)); + } catch (NativeLibrary.NoDiscKeyException exception) { + onEmulationError(getString(R.string.no_disk_key)); + } catch (NativeLibrary.NoTitleTikException exception) { + onEmulationError(getString(R.string.no_title_tik)); + } catch (NativeLibrary.GameFilesException exception) { + onEmulationError(getString(R.string.game_files_unknown_error, launchPath)); + } + } + + private void onEmulationError(String errorMessage) { + if (viewModel == null) + return; + viewModel.setEmulationError(new EmulationError(errorMessage)); + } } \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationViewModel.java b/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationViewModel.java new file mode 100644 index 00000000..686532cd --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationViewModel.java @@ -0,0 +1,19 @@ +package info.cemu.Cemu.emulation; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import java.util.Optional; + +public class EmulationViewModel extends ViewModel { + private final MutableLiveData emulationData = new MutableLiveData<>(new EmulationData(Optional.empty())); + + public void setEmulationError(EmulationError emulationError) { + emulationData.setValue(new EmulationData(Optional.of(emulationError))); + } + + public LiveData getEmulationData() { + return emulationData; + } +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/gameview/Game.java b/src/android/app/src/main/java/info/cemu/Cemu/gameview/Game.java index a907b725..6045e907 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/gameview/Game.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/gameview/Game.java @@ -2,26 +2,5 @@ package info.cemu.Cemu.gameview; import android.graphics.Bitmap; -public class Game { - Long titleId; - String title; - Bitmap icon; - - public Game(Long titleId, String title, Bitmap icon) { - this.titleId = titleId; - this.title = title; - this.icon = icon; - } - - public Bitmap getIcon() { - return icon; - } - - public Long getTitleId() { - return titleId; - } - - public String getTitle() { - return title; - } +public record Game(String path, String title, Bitmap icon) { } diff --git a/src/android/app/src/main/java/info/cemu/Cemu/gameview/GameAdapter.java b/src/android/app/src/main/java/info/cemu/Cemu/gameview/GameAdapter.java index 5e74f951..bfd4469b 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/gameview/GameAdapter.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/gameview/GameAdapter.java @@ -25,17 +25,17 @@ public class GameAdapter extends ListAdapter { public static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback<>() { @Override public boolean areItemsTheSame(@NonNull Game oldItem, @NonNull Game newItem) { - return oldItem.titleId.equals(newItem.titleId); + return oldItem.path().equals(newItem.path()); } @Override public boolean areContentsTheSame(@NonNull Game oldItem, @NonNull Game newItem) { - return oldItem.titleId.equals(newItem.titleId); + return oldItem.path().equals(newItem.path()); } }; public interface GameTitleClickAction { - void action(long titleId); + void action(String gamePath); } public GameAdapter(GameTitleClickAction gameTitleClickAction) { @@ -50,7 +50,7 @@ public class GameAdapter extends ListAdapter { super.submitList(orignalGameList); return; } - super.submitList(orignalGameList.stream().filter(g -> g.title.toLowerCase(Locale.US).contains(this.filterText)).collect(Collectors.toList())); + super.submitList(orignalGameList.stream().filter(g -> g.title().toLowerCase(Locale.US).contains(this.filterText)).collect(Collectors.toList())); } @NonNull @@ -64,11 +64,11 @@ public class GameAdapter extends ListAdapter { public void onBindViewHolder(@NonNull GameAdapter.ViewHolder holder, int position) { Game game = getItem(position); if (game != null) { - holder.icon.setImageBitmap(game.getIcon()); - holder.text.setText(game.getTitle()); + holder.icon.setImageBitmap(game.icon()); + holder.text.setText(game.title()); holder.itemView.setOnClickListener(v -> { - long titleId = game.titleId; - gameTitleClickAction.action(titleId); + String gamePath = game.path(); + gameTitleClickAction.action(gamePath); }); } } @@ -82,7 +82,7 @@ public class GameAdapter extends ListAdapter { super.submitList(orignalGameList); return; } - super.submitList(orignalGameList.stream().filter(g -> g.title.toLowerCase(Locale.US).contains(this.filterText)).collect(Collectors.toList())); + super.submitList(orignalGameList.stream().filter(g -> g.title().toLowerCase(Locale.US).contains(this.filterText)).collect(Collectors.toList())); } public static class ViewHolder extends RecyclerView.ViewHolder { diff --git a/src/android/app/src/main/java/info/cemu/Cemu/gameview/GameViewModel.java b/src/android/app/src/main/java/info/cemu/Cemu/gameview/GameViewModel.java index dd7bc79b..45006439 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/gameview/GameViewModel.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/gameview/GameViewModel.java @@ -25,11 +25,11 @@ public class GameViewModel extends ViewModel { public GameViewModel() { this.gamesData = new MutableLiveData<>(); - NativeLibrary.setGameTitleLoadedCallback((titleId, title, colors, width, height) -> { + NativeLibrary.setGameTitleLoadedCallback((path, title, colors, width, height) -> { Bitmap icon = null; if (colors != null) icon = Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888); - Game game = new Game(titleId, title, icon); + Game game = new Game(path, title, icon); synchronized (GameViewModel.this) { games.add(game); gamesData.postValue(new ArrayList<>(games)); diff --git a/src/android/app/src/main/java/info/cemu/Cemu/gameview/GamesFragment.java b/src/android/app/src/main/java/info/cemu/Cemu/gameview/GamesFragment.java index 9b746830..a621c3c1 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/gameview/GamesFragment.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/gameview/GamesFragment.java @@ -27,7 +27,7 @@ public class GamesFragment extends Fragment { super.onCreate(savedInstanceState); gameAdapter = new GameAdapter(titleId -> { Intent intent = new Intent(getContext(), EmulationActivity.class); - intent.putExtra(EmulationActivity.GAME_TITLE_ID, titleId); + intent.putExtra(EmulationActivity.LAUNCH_PATH, titleId); startActivity(intent); }); gameViewModel = new ViewModelProvider(this).get(GameViewModel.class); diff --git a/src/android/app/src/main/java/info/cemu/Cemu/settings/gamespath/GamePathsFragment.java b/src/android/app/src/main/java/info/cemu/Cemu/settings/gamespath/GamePathsFragment.java index d267c28f..98edc1a1 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/settings/gamespath/GamePathsFragment.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/settings/gamespath/GamePathsFragment.java @@ -40,31 +40,30 @@ public class GamePathsFragment extends Fragment { public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); folderSelectionLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { - if (result.getResultCode() == RESULT_OK) { - Intent data = result.getData(); - if (data != null) { - Uri uri = Objects.requireNonNull(data.getData()); - requireActivity().getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - DocumentFile documentFile = DocumentFile.fromTreeUri(requireContext(), uri); - if (documentFile == null) return; - String gamesPath = documentFile.getUri().toString(); - if (gamesPaths.stream().anyMatch(p -> p.equals(gamesPath))) { - Toast.makeText(requireContext(), R.string.game_path_already_added, Toast.LENGTH_LONG).show(); - return; - } - NativeLibrary.addGamesPath(gamesPath); - gamesPaths = Stream.concat(Stream.of(gamesPath), gamesPaths.stream()).collect(Collectors.toList()); - gamePathAdapter.submitList(gamesPaths); - } + if (result.getResultCode() != RESULT_OK) { + return; } + Intent data = result.getData(); + if (data == null) { + return; + } + Uri uri = Objects.requireNonNull(data.getData()); + requireActivity().getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + DocumentFile documentFile = DocumentFile.fromTreeUri(requireContext(), uri); + if (documentFile == null) return; + String gamesPath = documentFile.getUri().toString(); + if (gamesPaths.stream().anyMatch(p -> p.equals(gamesPath))) { + Toast.makeText(requireContext(), R.string.game_path_already_added, Toast.LENGTH_LONG).show(); + return; + } + NativeLibrary.addGamesPath(gamesPath); + gamesPaths = Stream.concat(Stream.of(gamesPath), gamesPaths.stream()).collect(Collectors.toList()); + gamePathAdapter.submitList(gamesPaths); }); - gamePathAdapter = new GamePathAdapter(new GamePathAdapter.OnRemoveGamePath() { - @Override - public void onRemoveGamePath(String path) { - NativeLibrary.removeGamesPath(path); - gamesPaths = gamesPaths.stream().filter(p -> !p.equals(path)).collect(Collectors.toList()); - gamePathAdapter.submitList(gamesPaths); - } + gamePathAdapter = new GamePathAdapter(path -> { + NativeLibrary.removeGamesPath(path); + gamesPaths = gamesPaths.stream().filter(p -> !p.equals(path)).collect(Collectors.toList()); + gamePathAdapter.submitList(gamesPaths); }); } diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index fc5545fb..20d4b87c 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -168,4 +168,10 @@ Remove game path Game paths Game path already added + Quit + Error + Unable to launch game\nPath: %1$s + Could not decrypt title. Make sure that keys.txt contains the correct disc key for this title. + Could not decrypt title because title.tik is missing. + Unable to launch game because the base files were not found. \ No newline at end of file