Added support for opening titles using file managers

This commit is contained in:
SSimco
2024-09-21 20:48:39 +03:00
parent cc10f93030
commit 80ea25b10f
20 changed files with 300 additions and 111 deletions
+23 -2
View File
@@ -9,10 +9,10 @@
<application
android:name="info.cemu.Cemu.CemuApplication"
android:allowBackup="true"
android:appCategory="game"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:appCategory="game"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
@@ -21,10 +21,31 @@
<activity
android:name=".emulation.EmulationActivity"
android:configChanges="orientation|screenSize"
android:exported="true"
android:launchMode="singleTop"
android:parentActivityName=".MainActivity"
android:screenOrientation="userLandscape"
tools:ignore="DiscouragedApi" />
tools:ignore="DiscouragedApi">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="content" />
<data android:mimeType="*/*" />
<data android:host="*" />
<data android:pathPattern=".*\\.wud" />
<data android:pathPattern=".*\\.wux" />
<data android:pathPattern=".*\\.wua" />
<data android:pathPattern=".*\\.wuhb" />
<data android:pathPattern=".*\\.iso" />
<data android:pathPattern=".*\\.elf" />
<data android:pathPattern=".*\\.rpx" />
<!--
TODO?
<data android:pathPattern=".*/title.tmd" />
-->
</intent-filter>
</activity>
<activity
android:name=".settings.SettingsActivity"
android:exported="false"
@@ -17,7 +17,7 @@ class AndroidGameTitleLoadedCallback : public GameTitleLoadedCallback
{
JNIUtils::ScopedJNIENV env;
jstring name = env->NewStringUTF(game.name.c_str());
jlong titleId = static_cast<const jlong>(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<const jint*>(icon->intColors()));
}
env->CallVoidMethod(*m_gameTitleLoadedCallbackObj, m_onGameTitleLoadedMID, static_cast<jlong>(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);
}
};
@@ -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();
}
+45 -2
View File
@@ -4,5 +4,48 @@
namespace CafeSystemUtils
{
void startGame(TitleId titleId);
};
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
@@ -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()
@@ -72,6 +72,8 @@ void GameTitleLoader::titleRefresh(TitleId titleId)
Game& game = m_gameInfos[baseTitleId];
std::optional<TitleInfo> 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();
@@ -8,6 +8,7 @@
struct Game
{
std::string name;
std::optional<fs::path> 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();
+23 -3
View File
@@ -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<TitleId>(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<AndroidGameTitleLoadedCallback>(onGameTitleLoadedMID, game_title_loaded_callback));
}
@@ -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);
@@ -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<EmulationData> {
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());
}
}
}
@@ -0,0 +1,6 @@
package info.cemu.Cemu.emulation;
import java.util.Optional;
public record EmulationData(Optional<EmulationError> emulationError) {
}
@@ -0,0 +1,4 @@
package info.cemu.Cemu.emulation;
public record EmulationError(String errorMessage) {
}
@@ -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));
}
}
@@ -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> emulationData = new MutableLiveData<>(new EmulationData(Optional.empty()));
public void setEmulationError(EmulationError emulationError) {
emulationData.setValue(new EmulationData(Optional.of(emulationError)));
}
public LiveData<EmulationData> getEmulationData() {
return emulationData;
}
}
@@ -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) {
}
@@ -25,17 +25,17 @@ public class GameAdapter extends ListAdapter<Game, GameAdapter.ViewHolder> {
public static final DiffUtil.ItemCallback<Game> 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<Game, GameAdapter.ViewHolder> {
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<Game, GameAdapter.ViewHolder> {
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<Game, GameAdapter.ViewHolder> {
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 {
@@ -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));
@@ -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);
@@ -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);
});
}
@@ -168,4 +168,10 @@
<string name="remove_game_path">Remove game path</string>
<string name="game_paths_settings">Game paths</string>
<string name="game_path_already_added">Game path already added</string>
<string name="quit">Quit</string>
<string name="error">Error</string>
<string name="game_files_unknown_error">Unable to launch game\nPath: %1$s</string>
<string name="no_disk_key">Could not decrypt title. Make sure that keys.txt contains the correct disc key for this title.</string>
<string name="no_title_tik">Could not decrypt title because title.tik is missing.</string>
<string name="game_not_found">Unable to launch game because the base files were not found.</string>
</resources>