summaryrefslogtreecommitdiff
path: root/Source/Android/app/src/main/java/org
diff options
context:
space:
mode:
authorLéo Lam <leo@leolam.fr>2021-09-16 19:21:33 +0200
committerGitHub <noreply@github.com>2021-09-16 19:21:33 +0200
commit73794506336a17102c4df0419a099734a7ef3a1d (patch)
tree52e557891f254f09c77645afed76f125686f4218 /Source/Android/app/src/main/java/org
parent4d1bd54917f097510e814d0ffcd625de6867fcc0 (diff)
parent0ca4c6dbd4a19607b2fc82f82171226c679f58b4 (diff)
Merge pull request #10092 from JosJuice/android-cheats
Android: Add cheat GUI
Diffstat (limited to 'Source/Android/app/src/main/java/org')
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GamePropertiesDialog.java13
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/ARCheat.java60
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/AbstractCheat.java62
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/Cheat.java48
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/CheatsViewModel.java297
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/GeckoCheat.java78
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/PatchCheat.java60
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/ActionViewHolder.java67
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatDetailsFragment.java198
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatItem.java49
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatItemViewHolder.java20
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatListFragment.java43
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatViewHolder.java64
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatWarningFragment.java63
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsActivity.java253
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsAdapter.java162
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/HeaderViewHolder.java28
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.java2
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java10
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.java2
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/TwoPaneOnBackPressedCallback.java46
21 files changed, 1614 insertions, 11 deletions
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GamePropertiesDialog.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GamePropertiesDialog.java
index e550e30b2d..8a92ffba28 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GamePropertiesDialog.java
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GamePropertiesDialog.java
@@ -12,6 +12,7 @@ import androidx.fragment.app.DialogFragment;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.activities.ConvertActivity;
+import org.dolphinemu.dolphinemu.features.cheats.ui.CheatsActivity;
import org.dolphinemu.dolphinemu.features.settings.model.Settings;
import org.dolphinemu.dolphinemu.features.settings.model.StringSetting;
import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag;
@@ -28,7 +29,8 @@ public class GamePropertiesDialog extends DialogFragment
{
public static final String TAG = "GamePropertiesDialog";
private static final String ARG_PATH = "path";
- private static final String ARG_GAMEID = "game_id";
+ private static final String ARG_GAME_ID = "game_id";
+ private static final String ARG_GAMETDB_ID = "gametdb_id";
public static final String ARG_REVISION = "revision";
private static final String ARG_PLATFORM = "platform";
private static final String ARG_SHOULD_ALLOW_CONVERSION = "should_allow_conversion";
@@ -39,7 +41,8 @@ public class GamePropertiesDialog extends DialogFragment
Bundle arguments = new Bundle();
arguments.putString(ARG_PATH, gameFile.getPath());
- arguments.putString(ARG_GAMEID, gameFile.getGameId());
+ arguments.putString(ARG_GAME_ID, gameFile.getGameId());
+ arguments.putString(ARG_GAMETDB_ID, gameFile.getGameTdbId());
arguments.putInt(ARG_REVISION, gameFile.getRevision());
arguments.putInt(ARG_PLATFORM, gameFile.getPlatform());
arguments.putBoolean(ARG_SHOULD_ALLOW_CONVERSION, gameFile.shouldAllowConversion());
@@ -53,7 +56,8 @@ public class GamePropertiesDialog extends DialogFragment
public Dialog onCreateDialog(Bundle savedInstanceState)
{
final String path = requireArguments().getString(ARG_PATH);
- final String gameId = requireArguments().getString(ARG_GAMEID);
+ final String gameId = requireArguments().getString(ARG_GAME_ID);
+ final String gameTdbId = requireArguments().getString(ARG_GAMETDB_ID);
final int revision = requireArguments().getInt(ARG_REVISION);
final int platform = requireArguments().getInt(ARG_PLATFORM);
final boolean shouldAllowConversion =
@@ -91,6 +95,9 @@ public class GamePropertiesDialog extends DialogFragment
itemsBuilder.add(R.string.properties_edit_game_settings, (dialog, i) ->
SettingsActivity.launch(getContext(), MenuTag.SETTINGS, gameId, revision, isWii));
+ itemsBuilder.add(R.string.properties_edit_cheats, (dialog, i) ->
+ CheatsActivity.launch(getContext(), gameId, gameTdbId, revision, isWii));
+
itemsBuilder.add(R.string.properties_clear_game_settings, (dialog, i) ->
clearGameSettings(gameId));
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/ARCheat.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/ARCheat.java
new file mode 100644
index 0000000000..8a168e8bc5
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/ARCheat.java
@@ -0,0 +1,60 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.cheats.model;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+
+public class ARCheat extends AbstractCheat
+{
+ @Keep
+ private final long mPointer;
+
+ public ARCheat()
+ {
+ mPointer = createNew();
+ }
+
+ @Keep
+ private ARCheat(long pointer)
+ {
+ mPointer = pointer;
+ }
+
+ @Override
+ public native void finalize();
+
+ private native long createNew();
+
+ public boolean supportsCreator()
+ {
+ return false;
+ }
+
+ public boolean supportsNotes()
+ {
+ return false;
+ }
+
+ @NonNull
+ public native String getName();
+
+ @NonNull
+ public native String getCode();
+
+ public native boolean getUserDefined();
+
+ public native boolean getEnabled();
+
+ @Override
+ protected native int trySetImpl(@NonNull String name, @NonNull String creator,
+ @NonNull String notes, @NonNull String code);
+
+ @Override
+ protected native void setEnabledImpl(boolean enabled);
+
+ @NonNull
+ public static native ARCheat[] loadCodes(String gameId, int revision);
+
+ public static native void saveCodes(String gameId, int revision, ARCheat[] codes);
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/AbstractCheat.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/AbstractCheat.java
new file mode 100644
index 0000000000..9a86d58533
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/AbstractCheat.java
@@ -0,0 +1,62 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.cheats.model;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public abstract class AbstractCheat implements Cheat
+{
+ private Runnable mChangedCallback = null;
+
+ public int trySet(@NonNull String name, @NonNull String creator, @NonNull String notes,
+ @NonNull String code)
+ {
+ if (!code.isEmpty() && code.charAt(0) == '$')
+ {
+ int firstLineEnd = code.indexOf('\n');
+ if (firstLineEnd == -1)
+ {
+ name = code.substring(1);
+ code = "";
+ }
+ else
+ {
+ name = code.substring(1, firstLineEnd);
+ code = code.substring(firstLineEnd + 1);
+ }
+ }
+
+ if (name.isEmpty())
+ return TRY_SET_FAIL_NO_NAME;
+
+ int result = trySetImpl(name, creator, notes, code);
+
+ if (result == TRY_SET_SUCCESS)
+ onChanged();
+
+ return result;
+ }
+
+ public void setEnabled(boolean enabled)
+ {
+ setEnabledImpl(enabled);
+ onChanged();
+ }
+
+ public void setChangedCallback(@Nullable Runnable callback)
+ {
+ mChangedCallback = callback;
+ }
+
+ protected void onChanged()
+ {
+ if (mChangedCallback != null)
+ mChangedCallback.run();
+ }
+
+ protected abstract int trySetImpl(@NonNull String name, @NonNull String creator,
+ @NonNull String notes, @NonNull String code);
+
+ protected abstract void setEnabledImpl(boolean enabled);
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/Cheat.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/Cheat.java
new file mode 100644
index 0000000000..75a3befc6f
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/Cheat.java
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.cheats.model;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public interface Cheat
+{
+ int TRY_SET_FAIL_CODE_MIXED_ENCRYPTION = -3;
+ int TRY_SET_FAIL_NO_CODE_LINES = -2;
+ int TRY_SET_FAIL_NO_NAME = -1;
+ int TRY_SET_SUCCESS = 0;
+ // Result codes greater than 0 represent an error on the corresponding code line (one-indexed)
+
+ boolean supportsCreator();
+
+ boolean supportsNotes();
+
+ @NonNull
+ String getName();
+
+ @NonNull
+ default String getCreator()
+ {
+ return "";
+ }
+
+ @NonNull
+ default String getNotes()
+ {
+ return "";
+ }
+
+ @NonNull
+ String getCode();
+
+ int trySet(@NonNull String name, @NonNull String creator, @NonNull String notes,
+ @NonNull String code);
+
+ boolean getUserDefined();
+
+ boolean getEnabled();
+
+ void setEnabled(boolean enabled);
+
+ void setChangedCallback(@Nullable Runnable callback);
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/CheatsViewModel.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/CheatsViewModel.java
new file mode 100644
index 0000000000..0238628319
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/CheatsViewModel.java
@@ -0,0 +1,297 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.cheats.model;
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+public class CheatsViewModel extends ViewModel
+{
+ private boolean mLoaded = false;
+
+ private int mSelectedCheatPosition = -1;
+ private final MutableLiveData<Cheat> mSelectedCheat = new MutableLiveData<>(null);
+ private final MutableLiveData<Boolean> mIsAdding = new MutableLiveData<>(false);
+ private final MutableLiveData<Boolean> mIsEditing = new MutableLiveData<>(false);
+
+ private final MutableLiveData<Integer> mCheatAddedEvent = new MutableLiveData<>(null);
+ private final MutableLiveData<Integer> mCheatChangedEvent = new MutableLiveData<>(null);
+ private final MutableLiveData<Integer> mCheatDeletedEvent = new MutableLiveData<>(null);
+ private final MutableLiveData<Integer> mGeckoCheatsDownloadedEvent = new MutableLiveData<>(null);
+ private final MutableLiveData<Boolean> mOpenDetailsViewEvent = new MutableLiveData<>(false);
+
+ private ArrayList<PatchCheat> mPatchCheats;
+ private ArrayList<ARCheat> mARCheats;
+ private ArrayList<GeckoCheat> mGeckoCheats;
+
+ private boolean mPatchCheatsNeedSaving = false;
+ private boolean mARCheatsNeedSaving = false;
+ private boolean mGeckoCheatsNeedSaving = false;
+
+ public void load(String gameID, int revision)
+ {
+ if (mLoaded)
+ return;
+
+ mPatchCheats = new ArrayList<>();
+ Collections.addAll(mPatchCheats, PatchCheat.loadCodes(gameID, revision));
+ mARCheats = new ArrayList<>();
+ Collections.addAll(mARCheats, ARCheat.loadCodes(gameID, revision));
+ mGeckoCheats = new ArrayList<>();
+ Collections.addAll(mGeckoCheats, GeckoCheat.loadCodes(gameID, revision));
+
+ for (PatchCheat cheat : mPatchCheats)
+ {
+ cheat.setChangedCallback(() -> mPatchCheatsNeedSaving = true);
+ }
+ for (ARCheat cheat : mARCheats)
+ {
+ cheat.setChangedCallback(() -> mARCheatsNeedSaving = true);
+ }
+ for (GeckoCheat cheat : mGeckoCheats)
+ {
+ cheat.setChangedCallback(() -> mGeckoCheatsNeedSaving = true);
+ }
+
+ mLoaded = true;
+ }
+
+ public void saveIfNeeded(String gameID, int revision)
+ {
+ if (mPatchCheatsNeedSaving)
+ {
+ PatchCheat.saveCodes(gameID, revision, mPatchCheats.toArray(new PatchCheat[0]));
+ mPatchCheatsNeedSaving = false;
+ }
+
+ if (mARCheatsNeedSaving)
+ {
+ ARCheat.saveCodes(gameID, revision, mARCheats.toArray(new ARCheat[0]));
+ mARCheatsNeedSaving = false;
+ }
+
+ if (mGeckoCheatsNeedSaving)
+ {
+ GeckoCheat.saveCodes(gameID, revision, mGeckoCheats.toArray(new GeckoCheat[0]));
+ mGeckoCheatsNeedSaving = false;
+ }
+ }
+
+ public LiveData<Cheat> getSelectedCheat()
+ {
+ return mSelectedCheat;
+ }
+
+ public void setSelectedCheat(Cheat cheat, int position)
+ {
+ if (mIsEditing.getValue())
+ setIsEditing(false);
+
+ mSelectedCheat.setValue(cheat);
+ mSelectedCheatPosition = position;
+ }
+
+ public LiveData<Boolean> getIsAdding()
+ {
+ return mIsAdding;
+ }
+
+ public void startAddingCheat(Cheat cheat, int position)
+ {
+ mSelectedCheat.setValue(cheat);
+ mSelectedCheatPosition = position;
+
+ mIsAdding.setValue(true);
+ mIsEditing.setValue(true);
+ }
+
+ public void finishAddingCheat()
+ {
+ if (!mIsAdding.getValue())
+ throw new IllegalStateException();
+
+ mIsAdding.setValue(false);
+ mIsEditing.setValue(false);
+
+ Cheat cheat = mSelectedCheat.getValue();
+
+ if (cheat instanceof PatchCheat)
+ {
+ mPatchCheats.add((PatchCheat) mSelectedCheat.getValue());
+ cheat.setChangedCallback(() -> mPatchCheatsNeedSaving = true);
+ mPatchCheatsNeedSaving = true;
+ }
+ else if (cheat instanceof ARCheat)
+ {
+ mARCheats.add((ARCheat) mSelectedCheat.getValue());
+ cheat.setChangedCallback(() -> mPatchCheatsNeedSaving = true);
+ mARCheatsNeedSaving = true;
+ }
+ else if (cheat instanceof GeckoCheat)
+ {
+ mGeckoCheats.add((GeckoCheat) mSelectedCheat.getValue());
+ cheat.setChangedCallback(() -> mGeckoCheatsNeedSaving = true);
+ mGeckoCheatsNeedSaving = true;
+ }
+ else
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ notifyCheatAdded();
+ }
+
+ public LiveData<Boolean> getIsEditing()
+ {
+ return mIsEditing;
+ }
+
+ public void setIsEditing(boolean isEditing)
+ {
+ mIsEditing.setValue(isEditing);
+
+ if (mIsAdding.getValue() && !isEditing)
+ {
+ mIsAdding.setValue(false);
+ setSelectedCheat(null, -1);
+ }
+ }
+
+ /**
+ * When a cheat is added, the integer stored in the returned LiveData
+ * changes to the position of that cheat, then changes back to null.
+ */
+ public LiveData<Integer> getCheatAddedEvent()
+ {
+ return mCheatAddedEvent;
+ }
+
+ private void notifyCheatAdded()
+ {
+ mCheatAddedEvent.setValue(mSelectedCheatPosition);
+ mCheatAddedEvent.setValue(null);
+ }
+
+ /**
+ * When a cheat is edited, the integer stored in the returned LiveData
+ * changes to the position of that cheat, then changes back to null.
+ */
+ public LiveData<Integer> getCheatChangedEvent()
+ {
+ return mCheatChangedEvent;
+ }
+
+ /**
+ * Notifies that an edit has been made to the contents of the currently selected cheat.
+ */
+ public void notifySelectedCheatChanged()
+ {
+ notifyCheatChanged(mSelectedCheatPosition);
+ }
+
+ /**
+ * Notifies that an edit has been made to the contents of the cheat at the given position.
+ */
+ public void notifyCheatChanged(int position)
+ {
+ mCheatChangedEvent.setValue(position);
+ mCheatChangedEvent.setValue(null);
+ }
+
+ /**
+ * When a cheat is deleted, the integer stored in the returned LiveData
+ * changes to the position of that cheat, then changes back to null.
+ */
+ public LiveData<Integer> getCheatDeletedEvent()
+ {
+ return mCheatDeletedEvent;
+ }
+
+ public void deleteSelectedCheat()
+ {
+ Cheat cheat = mSelectedCheat.getValue();
+ int position = mSelectedCheatPosition;
+
+ setSelectedCheat(null, -1);
+
+ if (mPatchCheats.remove(cheat))
+ mPatchCheatsNeedSaving = true;
+ if (mARCheats.remove(cheat))
+ mARCheatsNeedSaving = true;
+ if (mGeckoCheats.remove(cheat))
+ mGeckoCheatsNeedSaving = true;
+
+ notifyCheatDeleted(position);
+ }
+
+ /**
+ * Notifies that the cheat at the given position has been deleted.
+ */
+ private void notifyCheatDeleted(int position)
+ {
+ mCheatDeletedEvent.setValue(position);
+ mCheatDeletedEvent.setValue(null);
+ }
+
+ /**
+ * When Gecko cheats are downloaded, the integer stored in the returned LiveData
+ * changes to the number of cheats added, then changes back to null.
+ */
+ public LiveData<Integer> getGeckoCheatsDownloadedEvent()
+ {
+ return mGeckoCheatsDownloadedEvent;
+ }
+
+ public int addDownloadedGeckoCodes(GeckoCheat[] cheats)
+ {
+ int cheatsAdded = 0;
+
+ for (GeckoCheat cheat : cheats)
+ {
+ if (!mGeckoCheats.contains(cheat))
+ {
+ mGeckoCheats.add(cheat);
+ cheatsAdded++;
+ }
+ }
+
+ if (cheatsAdded != 0)
+ {
+ mGeckoCheatsNeedSaving = true;
+ mGeckoCheatsDownloadedEvent.setValue(cheatsAdded);
+ mGeckoCheatsDownloadedEvent.setValue(null);
+ }
+
+ return cheatsAdded;
+ }
+
+ public LiveData<Boolean> getOpenDetailsViewEvent()
+ {
+ return mOpenDetailsViewEvent;
+ }
+
+ public void openDetailsView()
+ {
+ mOpenDetailsViewEvent.setValue(true);
+ mOpenDetailsViewEvent.setValue(false);
+ }
+
+ public ArrayList<PatchCheat> getPatchCheats()
+ {
+ return mPatchCheats;
+ }
+
+ public ArrayList<ARCheat> getARCheats()
+ {
+ return mARCheats;
+ }
+
+ public ArrayList<GeckoCheat> getGeckoCheats()
+ {
+ return mGeckoCheats;
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/GeckoCheat.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/GeckoCheat.java
new file mode 100644
index 0000000000..4397a95d81
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/GeckoCheat.java
@@ -0,0 +1,78 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.cheats.model;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class GeckoCheat extends AbstractCheat
+{
+ @Keep
+ private final long mPointer;
+
+ public GeckoCheat()
+ {
+ mPointer = createNew();
+ }
+
+ @Keep
+ private GeckoCheat(long pointer)
+ {
+ mPointer = pointer;
+ }
+
+ @Override
+ public native void finalize();
+
+ private native long createNew();
+
+ @Override
+ public boolean equals(@Nullable Object obj)
+ {
+ return obj != null && getClass() == obj.getClass() && equalsImpl((GeckoCheat) obj);
+ }
+
+ public boolean supportsCreator()
+ {
+ return true;
+ }
+
+ public boolean supportsNotes()
+ {
+ return true;
+ }
+
+ @NonNull
+ public native String getName();
+
+ @NonNull
+ public native String getCreator();
+
+ @NonNull
+ public native String getNotes();
+
+ @NonNull
+ public native String getCode();
+
+ public native boolean getUserDefined();
+
+ public native boolean getEnabled();
+
+ public native boolean equalsImpl(@NonNull GeckoCheat other);
+
+ @Override
+ protected native int trySetImpl(@NonNull String name, @NonNull String creator,
+ @NonNull String notes, @NonNull String code);
+
+ @Override
+ protected native void setEnabledImpl(boolean enabled);
+
+ @NonNull
+ public static native GeckoCheat[] loadCodes(String gameId, int revision);
+
+ public static native void saveCodes(String gameId, int revision, GeckoCheat[] codes);
+
+ @Nullable
+ public static native GeckoCheat[] downloadCodes(String gameTdbId);
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/PatchCheat.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/PatchCheat.java
new file mode 100644
index 0000000000..9dcb9f8111
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/PatchCheat.java
@@ -0,0 +1,60 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.cheats.model;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+
+public class PatchCheat extends AbstractCheat
+{
+ @Keep
+ private final long mPointer;
+
+ public PatchCheat()
+ {
+ mPointer = createNew();
+ }
+
+ @Keep
+ private PatchCheat(long pointer)
+ {
+ mPointer = pointer;
+ }
+
+ @Override
+ public native void finalize();
+
+ private native long createNew();
+
+ public boolean supportsCreator()
+ {
+ return false;
+ }
+
+ public boolean supportsNotes()
+ {
+ return false;
+ }
+
+ @NonNull
+ public native String getName();
+
+ @NonNull
+ public native String getCode();
+
+ public native boolean getUserDefined();
+
+ public native boolean getEnabled();
+
+ @Override
+ protected native int trySetImpl(@NonNull String name, @NonNull String creator,
+ @NonNull String notes, @NonNull String code);
+
+ @Override
+ protected native void setEnabledImpl(boolean enabled);
+
+ @NonNull
+ public static native PatchCheat[] loadCodes(String gameId, int revision);
+
+ public static native void saveCodes(String gameId, int revision, PatchCheat[] codes);
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/ActionViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/ActionViewHolder.java
new file mode 100644
index 0000000000..3e99384fdd
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/ActionViewHolder.java
@@ -0,0 +1,67 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.cheats.ui;
+
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.ViewModelProvider;
+
+import org.dolphinemu.dolphinemu.R;
+import org.dolphinemu.dolphinemu.features.cheats.model.ARCheat;
+import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel;
+import org.dolphinemu.dolphinemu.features.cheats.model.GeckoCheat;
+import org.dolphinemu.dolphinemu.features.cheats.model.PatchCheat;
+
+public class ActionViewHolder extends CheatItemViewHolder implements View.OnClickListener
+{
+ private final TextView mName;
+
+ private CheatsActivity mActivity;
+ private CheatsViewModel mViewModel;
+ private int mString;
+ private int mPosition;
+
+ public ActionViewHolder(@NonNull View itemView)
+ {
+ super(itemView);
+
+ mName = itemView.findViewById(R.id.text_setting_name);
+
+ itemView.setOnClickListener(this);
+ }
+
+ public void bind(CheatsActivity activity, CheatItem item, int position)
+ {
+ mActivity = activity;
+ mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
+ mString = item.getString();
+ mPosition = position;
+
+ mName.setText(mString);
+ }
+
+ public void onClick(View root)
+ {
+ if (mString == R.string.cheats_add_ar)
+ {
+ mViewModel.startAddingCheat(new ARCheat(), mPosition);
+ mViewModel.openDetailsView();
+ }
+ else if (mString == R.string.cheats_add_gecko)
+ {
+ mViewModel.startAddingCheat(new GeckoCheat(), mPosition);
+ mViewModel.openDetailsView();
+ }
+ else if (mString == R.string.cheats_add_patch)
+ {
+ mViewModel.startAddingCheat(new PatchCheat(), mPosition);
+ mViewModel.openDetailsView();
+ }
+ else if (mString == R.string.cheats_download_gecko)
+ {
+ mActivity.downloadGeckoCodes();
+ }
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatDetailsFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatDetailsFragment.java
new file mode 100644
index 0000000000..2ab6bb31b0
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatDetailsFragment.java
@@ -0,0 +1,198 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.cheats.ui;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.ViewModelProvider;
+
+import org.dolphinemu.dolphinemu.R;
+import org.dolphinemu.dolphinemu.features.cheats.model.Cheat;
+import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel;
+
+public class CheatDetailsFragment extends Fragment
+{
+ private View mRoot;
+ private ScrollView mScrollView;
+ private TextView mLabelName;
+ private EditText mEditName;
+ private TextView mLabelCreator;
+ private EditText mEditCreator;
+ private TextView mLabelNotes;
+ private EditText mEditNotes;
+ private EditText mEditCode;
+ private Button mButtonDelete;
+ private Button mButtonEdit;
+ private Button mButtonCancel;
+ private Button mButtonOk;
+
+ private CheatsViewModel mViewModel;
+ private Cheat mCheat;
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState)
+ {
+ return inflater.inflate(R.layout.fragment_cheat_details, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState)
+ {
+ mRoot = view.findViewById(R.id.root);
+ mScrollView = view.findViewById(R.id.scroll_view);
+ mLabelName = view.findViewById(R.id.label_name);
+ mEditName = view.findViewById(R.id.edit_name);
+ mLabelCreator = view.findViewById(R.id.label_creator);
+ mEditCreator = view.findViewById(R.id.edit_creator);
+ mLabelNotes = view.findViewById(R.id.label_notes);
+ mEditNotes = view.findViewById(R.id.edit_notes);
+ mEditCode = view.findViewById(R.id.edit_code);
+ mButtonDelete = view.findViewById(R.id.button_delete);
+ mButtonEdit = view.findViewById(R.id.button_edit);
+ mButtonCancel = view.findViewById(R.id.button_cancel);
+ mButtonOk = view.findViewById(R.id.button_ok);
+
+ CheatsActivity activity = (CheatsActivity) requireActivity();
+ mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
+
+ mViewModel.getSelectedCheat().observe(getViewLifecycleOwner(), this::onSelectedCheatUpdated);
+ mViewModel.getIsEditing().observe(getViewLifecycleOwner(), this::onIsEditingUpdated);
+
+ mButtonDelete.setOnClickListener(this::onDeleteClicked);
+ mButtonEdit.setOnClickListener(this::onEditClicked);
+ mButtonCancel.setOnClickListener(this::onCancelClicked);
+ mButtonOk.setOnClickListener(this::onOkClicked);
+
+ CheatsActivity.setOnFocusChangeListenerRecursively(view,
+ (v, hasFocus) -> activity.onDetailsViewFocusChange(hasFocus));
+ }
+
+ private void clearEditErrors()
+ {
+ mEditName.setError(null);
+ mEditCode.setError(null);
+ }
+
+ private void onDeleteClicked(View view)
+ {
+ AlertDialog.Builder builder =
+ new AlertDialog.Builder(requireContext(), R.style.DolphinDialogBase);
+ builder.setMessage(getString(R.string.cheats_delete_confirmation, mCheat.getName()));
+ builder.setPositiveButton(R.string.yes, (dialog, i) -> mViewModel.deleteSelectedCheat());
+ builder.setNegativeButton(R.string.no, null);
+ builder.show();
+ }
+
+ private void onEditClicked(View view)
+ {
+ mViewModel.setIsEditing(true);
+ mButtonOk.requestFocus();
+ }
+
+ private void onCancelClicked(View view)
+ {
+ mViewModel.setIsEditing(false);
+ onSelectedCheatUpdated(mCheat);
+ mButtonDelete.requestFocus();
+ }
+
+ private void onOkClicked(View view)
+ {
+ clearEditErrors();
+
+ int result = mCheat.trySet(mEditName.getText().toString(), mEditCreator.getText().toString(),
+ mEditNotes.getText().toString(), mEditCode.getText().toString());
+
+ switch (result)
+ {
+ case Cheat.TRY_SET_SUCCESS:
+ if (mViewModel.getIsAdding().getValue())
+ {
+ mViewModel.finishAddingCheat();
+ onSelectedCheatUpdated(mCheat);
+ }
+ else
+ {
+ mViewModel.notifySelectedCheatChanged();
+ mViewModel.setIsEditing(false);
+ }
+ mButtonEdit.requestFocus();
+ break;
+ case Cheat.TRY_SET_FAIL_NO_NAME:
+ mEditName.setError(getString(R.string.cheats_error_no_name));
+ mScrollView.smoothScrollTo(0, mLabelName.getTop());
+ break;
+ case Cheat.TRY_SET_FAIL_NO_CODE_LINES:
+ mEditCode.setError(getString(R.string.cheats_error_no_code_lines));
+ mScrollView.smoothScrollTo(0, mEditCode.getBottom());
+ break;
+ case Cheat.TRY_SET_FAIL_CODE_MIXED_ENCRYPTION:
+ mEditCode.setError(getString(R.string.cheats_error_mixed_encryption));
+ mScrollView.smoothScrollTo(0, mEditCode.getBottom());
+ break;
+ default:
+ mEditCode.setError(getString(R.string.cheats_error_on_line, result));
+ mScrollView.smoothScrollTo(0, mEditCode.getBottom());
+ break;
+ }
+ }
+
+ private void onSelectedCheatUpdated(@Nullable Cheat cheat)
+ {
+ clearEditErrors();
+
+ mRoot.setVisibility(cheat == null ? View.GONE : View.VISIBLE);
+
+ int creatorVisibility = cheat != null && cheat.supportsCreator() ? View.VISIBLE : View.GONE;
+ int notesVisibility = cheat != null && cheat.supportsNotes() ? View.VISIBLE : View.GONE;
+ mLabelCreator.setVisibility(creatorVisibility);
+ mEditCreator.setVisibility(creatorVisibility);
+ mLabelNotes.setVisibility(notesVisibility);
+ mEditNotes.setVisibility(notesVisibility);
+
+ boolean userDefined = cheat != null && cheat.getUserDefined();
+ mButtonDelete.setEnabled(userDefined);
+ mButtonEdit.setEnabled(userDefined);
+
+ // If the fragment was recreated while editing a cheat, it's vital that we
+ // don't repopulate the fields, otherwise the user's changes will be lost
+ boolean isEditing = mViewModel.getIsEditing().getValue();
+
+ if (!isEditing && cheat != null)
+ {
+ mEditName.setText(cheat.getName());
+ mEditCreator.setText(cheat.getCreator());
+ mEditNotes.setText(cheat.getNotes());
+ mEditCode.setText(cheat.getCode());
+ }
+
+ mCheat = cheat;
+ }
+
+ private void onIsEditingUpdated(boolean isEditing)
+ {
+ mEditName.setEnabled(isEditing);
+ mEditCreator.setEnabled(isEditing);
+ mEditNotes.setEnabled(isEditing);
+ mEditCode.setEnabled(isEditing);
+
+ mButtonDelete.setVisibility(isEditing ? View.GONE : View.VISIBLE);
+ mButtonEdit.setVisibility(isEditing ? View.GONE : View.VISIBLE);
+ mButtonCancel.setVisibility(isEditing ? View.VISIBLE : View.GONE);
+ mButtonOk.setVisibility(isEditing ? View.VISIBLE : View.GONE);
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatItem.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatItem.java
new file mode 100644
index 0000000000..b293cd84d1
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatItem.java
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.cheats.ui;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.dolphinemu.dolphinemu.features.cheats.model.Cheat;
+
+public class CheatItem
+{
+ public static final int TYPE_CHEAT = 0;
+ public static final int TYPE_HEADER = 1;
+ public static final int TYPE_ACTION = 2;
+
+ private final @Nullable Cheat mCheat;
+ private final int mString;
+ private final int mType;
+
+ public CheatItem(@NonNull Cheat cheat)
+ {
+ mCheat = cheat;
+ mString = 0;
+ mType = TYPE_CHEAT;
+ }
+
+ public CheatItem(int type, int string)
+ {
+ mCheat = null;
+ mString = string;
+ mType = type;
+ }
+
+ @Nullable
+ public Cheat getCheat()
+ {
+ return mCheat;
+ }
+
+ public int getString()
+ {
+ return mString;
+ }
+
+ public int getType()
+ {
+ return mType;
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatItemViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatItemViewHolder.java
new file mode 100644
index 0000000000..b4e38f4c1b
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatItemViewHolder.java
@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.cheats.ui;
+
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel;
+
+public abstract class CheatItemViewHolder extends RecyclerView.ViewHolder
+{
+ public CheatItemViewHolder(@NonNull View itemView)
+ {
+ super(itemView);
+ }
+
+ public abstract void bind(CheatsActivity activity, CheatItem item, int position);
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatListFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatListFragment.java
new file mode 100644
index 0000000000..efc446d50e
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatListFragment.java
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.cheats.ui;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import org.dolphinemu.dolphinemu.R;
+import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel;
+import org.dolphinemu.dolphinemu.ui.DividerItemDecoration;
+
+public class CheatListFragment extends Fragment
+{
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState)
+ {
+ return inflater.inflate(R.layout.fragment_cheat_list, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState)
+ {
+ RecyclerView recyclerView = view.findViewById(R.id.cheat_list);
+
+ CheatsActivity activity = (CheatsActivity) requireActivity();
+ CheatsViewModel viewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
+
+ recyclerView.setAdapter(new CheatsAdapter(activity, viewModel));
+ recyclerView.setLayoutManager(new LinearLayoutManager(activity));
+ recyclerView.addItemDecoration(new DividerItemDecoration(activity, null));
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatViewHolder.java
new file mode 100644
index 0000000000..58e391fb01
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatViewHolder.java
@@ -0,0 +1,64 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.cheats.ui;
+
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+
+import org.dolphinemu.dolphinemu.R;
+import org.dolphinemu.dolphinemu.features.cheats.model.Cheat;
+import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel;
+
+public class CheatViewHolder extends CheatItemViewHolder
+ implements View.OnClickListener, CompoundButton.OnCheckedChangeListener
+{
+ private final View mRoot;
+ private final TextView mName;
+ private final CheckBox mCheckbox;
+
+ private CheatsViewModel mViewModel;
+ private Cheat mCheat;
+ private int mPosition;
+
+ public CheatViewHolder(@NonNull View itemView)
+ {
+ super(itemView);
+
+ mRoot = itemView.findViewById(R.id.root);
+ mName = itemView.findViewById(R.id.text_name);
+ mCheckbox = itemView.findViewById(R.id.checkbox);
+ }
+
+ public void bind(CheatsActivity activity, CheatItem item, int position)
+ {
+ mCheckbox.setOnCheckedChangeListener(null);
+
+ mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
+ mCheat = item.getCheat();
+ mPosition = position;
+
+ mName.setText(mCheat.getName());
+ mCheckbox.setChecked(mCheat.getEnabled());
+
+ mRoot.setOnClickListener(this);
+ mCheckbox.setOnCheckedChangeListener(this);
+ }
+
+ public void onClick(View root)
+ {
+ mViewModel.setSelectedCheat(mCheat, mPosition);
+ mViewModel.openDetailsView();
+ }
+
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked)
+ {
+ mCheat.setEnabled(isChecked);
+ mViewModel.notifyCheatChanged(mPosition);
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatWarningFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatWarningFragment.java
new file mode 100644
index 0000000000..df6d9d94b0
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatWarningFragment.java
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.cheats.ui;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+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.R;
+import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting;
+import org.dolphinemu.dolphinemu.features.settings.model.Settings;
+import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag;
+import org.dolphinemu.dolphinemu.features.settings.ui.SettingsActivity;
+
+public class CheatWarningFragment extends Fragment implements View.OnClickListener
+{
+ private View mView;
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState)
+ {
+ return inflater.inflate(R.layout.fragment_cheat_warning, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState)
+ {
+ mView = view;
+
+ Button settingsButton = view.findViewById(R.id.button_settings);
+ settingsButton.setOnClickListener(this);
+
+ CheatsActivity activity = (CheatsActivity) requireActivity();
+ CheatsActivity.setOnFocusChangeListenerRecursively(view,
+ (v, hasFocus) -> activity.onListViewFocusChange(hasFocus));
+ }
+
+ @Override
+ public void onResume()
+ {
+ super.onResume();
+
+ CheatsActivity activity = (CheatsActivity) requireActivity();
+ try (Settings settings = activity.loadGameSpecificSettings())
+ {
+ boolean cheatsEnabled = BooleanSetting.MAIN_ENABLE_CHEATS.getBoolean(settings);
+ mView.setVisibility(cheatsEnabled ? View.GONE : View.VISIBLE);
+ }
+ }
+
+ public void onClick(View view)
+ {
+ SettingsActivity.launch(requireContext(), MenuTag.CONFIG_GENERAL);
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsActivity.java
new file mode 100644
index 0000000000..9eff4d1c9e
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsActivity.java
@@ -0,0 +1,253 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.cheats.ui;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.view.ViewCompat;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.slidingpanelayout.widget.SlidingPaneLayout;
+
+import org.dolphinemu.dolphinemu.R;
+import org.dolphinemu.dolphinemu.features.cheats.model.Cheat;
+import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel;
+import org.dolphinemu.dolphinemu.features.cheats.model.GeckoCheat;
+import org.dolphinemu.dolphinemu.features.settings.model.Settings;
+import org.dolphinemu.dolphinemu.ui.TwoPaneOnBackPressedCallback;
+import org.dolphinemu.dolphinemu.ui.main.MainPresenter;
+
+public class CheatsActivity extends AppCompatActivity
+ implements SlidingPaneLayout.PanelSlideListener
+{
+ private static final String ARG_GAME_ID = "game_id";
+ private static final String ARG_GAMETDB_ID = "gametdb_id";
+ private static final String ARG_REVISION = "revision";
+ private static final String ARG_IS_WII = "is_wii";
+
+ private String mGameId;
+ private String mGameTdbId;
+ private int mRevision;
+ private boolean mIsWii;
+ private CheatsViewModel mViewModel;
+
+ private SlidingPaneLayout mSlidingPaneLayout;
+ private View mCheatList;
+ private View mCheatDetails;
+
+ private View mCheatListLastFocus;
+ private View mCheatDetailsLastFocus;
+
+ public static void launch(Context context, String gameId, String gameTdbId, int revision,
+ boolean isWii)
+ {
+ Intent intent = new Intent(context, CheatsActivity.class);
+ intent.putExtra(ARG_GAME_ID, gameId);
+ intent.putExtra(ARG_GAMETDB_ID, gameTdbId);
+ intent.putExtra(ARG_REVISION, revision);
+ intent.putExtra(ARG_IS_WII, isWii);
+ context.startActivity(intent);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+
+ MainPresenter.skipRescanningLibrary();
+
+ Intent intent = getIntent();
+ mGameId = intent.getStringExtra(ARG_GAME_ID);
+ mGameTdbId = intent.getStringExtra(ARG_GAMETDB_ID);
+ mRevision = intent.getIntExtra(ARG_REVISION, 0);
+ mIsWii = intent.getBooleanExtra(ARG_IS_WII, true);
+
+ setTitle(getString(R.string.cheats_with_game_id, mGameId));
+
+ mViewModel = new ViewModelProvider(this).get(CheatsViewModel.class);
+ mViewModel.load(mGameId, mRevision);
+
+ setContentView(R.layout.activity_cheats);
+
+ mSlidingPaneLayout = findViewById(R.id.sliding_pane_layout);
+ mCheatList = findViewById(R.id.cheat_list);
+ mCheatDetails = findViewById(R.id.cheat_details);
+
+ mCheatListLastFocus = mCheatList;
+ mCheatDetailsLastFocus = mCheatDetails;
+
+ mSlidingPaneLayout.addPanelSlideListener(this);
+
+ getOnBackPressedDispatcher().addCallback(this,
+ new TwoPaneOnBackPressedCallback(mSlidingPaneLayout));
+
+ mViewModel.getSelectedCheat().observe(this, this::onSelectedCheatChanged);
+ onSelectedCheatChanged(mViewModel.getSelectedCheat().getValue());
+
+ mViewModel.getOpenDetailsViewEvent().observe(this, this::openDetailsView);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu)
+ {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.menu_settings, menu);
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item)
+ {
+ if (item.getItemId() == R.id.menu_save_exit)
+ {
+ finish();
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ protected void onStop()
+ {
+ super.onStop();
+
+ mViewModel.saveIfNeeded(mGameId, mRevision);
+ }
+
+ @Override
+ public void onPanelSlide(@NonNull View panel, float slideOffset)
+ {
+ }
+
+ @Override
+ public void onPanelOpened(@NonNull View panel)
+ {
+ boolean rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL;
+ mCheatDetailsLastFocus.requestFocus(rtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT);
+ }
+
+ @Override
+ public void onPanelClosed(@NonNull View panel)
+ {
+ boolean rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL;
+ mCheatListLastFocus.requestFocus(rtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT);
+ }
+
+ private void onSelectedCheatChanged(Cheat selectedCheat)
+ {
+ boolean cheatSelected = selectedCheat != null;
+
+ if (!cheatSelected && mSlidingPaneLayout.isOpen())
+ mSlidingPaneLayout.close();
+
+ mSlidingPaneLayout.setLockMode(cheatSelected ?
+ SlidingPaneLayout.LOCK_MODE_UNLOCKED : SlidingPaneLayout.LOCK_MODE_LOCKED_CLOSED);
+ }
+
+ public void onListViewFocusChange(boolean hasFocus)
+ {
+ if (hasFocus)
+ {
+ mCheatListLastFocus = mCheatList.findFocus();
+ if (mCheatListLastFocus == null)
+ throw new NullPointerException();
+
+ mSlidingPaneLayout.close();
+ }
+ }
+
+ public void onDetailsViewFocusChange(boolean hasFocus)
+ {
+ if (hasFocus)
+ {
+ mCheatDetailsLastFocus = mCheatDetails.findFocus();
+ if (mCheatDetailsLastFocus == null)
+ throw new NullPointerException();
+
+ mSlidingPaneLayout.open();
+ }
+ }
+
+ private void openDetailsView(boolean open)
+ {
+ if (open)
+ mSlidingPaneLayout.open();
+ }
+
+ public Settings loadGameSpecificSettings()
+ {
+ Settings settings = new Settings();
+ settings.loadSettings(null, mGameId, mRevision, mIsWii);
+ return settings;
+ }
+
+ public void downloadGeckoCodes()
+ {
+ AlertDialog progressDialog = new AlertDialog.Builder(this, R.style.DolphinDialogBase).create();
+ progressDialog.setTitle(R.string.cheats_downloading);
+ progressDialog.setCancelable(false);
+ progressDialog.show();
+
+ new Thread(() ->
+ {
+ GeckoCheat[] codes = GeckoCheat.downloadCodes(mGameTdbId);
+
+ runOnUiThread(() ->
+ {
+ progressDialog.dismiss();
+
+ if (codes == null)
+ {
+ new AlertDialog.Builder(this, R.style.DolphinDialogBase)
+ .setMessage(getString(R.string.cheats_download_failed))
+ .setPositiveButton(R.string.ok, null)
+ .show();
+ }
+ else if (codes.length == 0)
+ {
+ new AlertDialog.Builder(this, R.style.DolphinDialogBase)
+ .setMessage(getString(R.string.cheats_download_empty))
+ .setPositiveButton(R.string.ok, null)
+ .show();
+ }
+ else
+ {
+ int cheatsAdded = mViewModel.addDownloadedGeckoCodes(codes);
+ String message = getString(R.string.cheats_download_succeeded, codes.length, cheatsAdded);
+
+ new AlertDialog.Builder(this, R.style.DolphinDialogBase)
+ .setMessage(message)
+ .setPositiveButton(R.string.ok, null)
+ .show();
+ }
+ });
+ }).start();
+ }
+
+ public static void setOnFocusChangeListenerRecursively(@NonNull View view,
+ View.OnFocusChangeListener listener)
+ {
+ view.setOnFocusChangeListener(listener);
+
+ if (view instanceof ViewGroup)
+ {
+ ViewGroup viewGroup = (ViewGroup) view;
+ for (int i = 0; i < viewGroup.getChildCount(); i++)
+ {
+ View child = viewGroup.getChildAt(i);
+ setOnFocusChangeListenerRecursively(child, listener);
+ }
+ }
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsAdapter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsAdapter.java
new file mode 100644
index 0000000000..5994feab2a
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsAdapter.java
@@ -0,0 +1,162 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.cheats.ui;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.recyclerview.widget.RecyclerView;
+
+import org.dolphinemu.dolphinemu.R;
+import org.dolphinemu.dolphinemu.features.cheats.model.ARCheat;
+import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel;
+import org.dolphinemu.dolphinemu.features.cheats.model.GeckoCheat;
+import org.dolphinemu.dolphinemu.features.cheats.model.PatchCheat;
+
+import java.util.ArrayList;
+
+public class CheatsAdapter extends RecyclerView.Adapter<CheatItemViewHolder>
+{
+ private final CheatsActivity mActivity;
+ private final CheatsViewModel mViewModel;
+
+ public CheatsAdapter(CheatsActivity activity, CheatsViewModel viewModel)
+ {
+ mActivity = activity;
+ mViewModel = viewModel;
+
+ mViewModel.getCheatAddedEvent().observe(activity, (position) ->
+ {
+ if (position != null)
+ notifyItemInserted(position);
+ });
+
+ mViewModel.getCheatChangedEvent().observe(activity, (position) ->
+ {
+ if (position != null)
+ notifyItemChanged(position);
+ });
+
+ mViewModel.getCheatDeletedEvent().observe(activity, (position) ->
+ {
+ if (position != null)
+ notifyItemRemoved(position);
+ });
+
+ mViewModel.getGeckoCheatsDownloadedEvent().observe(activity, (cheatsAdded) ->
+ {
+ if (cheatsAdded != null)
+ {
+ int positionEnd = getItemCount() - 2; // Skip "Add Gecko Code" and "Download Gecko Codes"
+ int positionStart = positionEnd - cheatsAdded;
+ notifyItemRangeInserted(positionStart, cheatsAdded);
+ }
+ });
+ }
+
+ @NonNull
+ @Override
+ public CheatItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType)
+ {
+ LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+
+ switch (viewType)
+ {
+ case CheatItem.TYPE_CHEAT:
+ View cheatView = inflater.inflate(R.layout.list_item_cheat, parent, false);
+ addViewListeners(cheatView);
+ return new CheatViewHolder(cheatView);
+ case CheatItem.TYPE_HEADER:
+ View headerView = inflater.inflate(R.layout.list_item_header, parent, false);
+ addViewListeners(headerView);
+ return new HeaderViewHolder(headerView);
+ case CheatItem.TYPE_ACTION:
+ View actionView = inflater.inflate(R.layout.list_item_submenu, parent, false);
+ addViewListeners(actionView);
+ return new ActionViewHolder(actionView);
+ default:
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull CheatItemViewHolder holder, int position)
+ {
+ holder.bind(mActivity, getItemAt(position), position);
+ }
+
+ @Override
+ public int getItemCount()
+ {
+ return mViewModel.getARCheats().size() + mViewModel.getGeckoCheats().size() +
+ mViewModel.getPatchCheats().size() + 7;
+ }
+
+ @Override
+ public int getItemViewType(int position)
+ {
+ return getItemAt(position).getType();
+ }
+
+ private void addViewListeners(View view)
+ {
+ CheatsActivity.setOnFocusChangeListenerRecursively(view,
+ (v, hasFocus) -> mActivity.onListViewFocusChange(hasFocus));
+ }
+
+ private CheatItem getItemAt(int position)
+ {
+ // Patches
+
+ if (position == 0)
+ return new CheatItem(CheatItem.TYPE_HEADER, R.string.cheats_header_patch);
+ position -= 1;
+
+ ArrayList<PatchCheat> patchCheats = mViewModel.getPatchCheats();
+ if (position < patchCheats.size())
+ return new CheatItem(patchCheats.get(position));
+ position -= patchCheats.size();
+
+ if (position == 0)
+ return new CheatItem(CheatItem.TYPE_ACTION, R.string.cheats_add_patch);
+ position -= 1;
+
+ // AR codes
+
+ if (position == 0)
+ return new CheatItem(CheatItem.TYPE_HEADER, R.string.cheats_header_ar);
+ position -= 1;
+
+ ArrayList<ARCheat> arCheats = mViewModel.getARCheats();
+ if (position < arCheats.size())
+ return new CheatItem(arCheats.get(position));
+ position -= arCheats.size();
+
+ if (position == 0)
+ return new CheatItem(CheatItem.TYPE_ACTION, R.string.cheats_add_ar);
+ position -= 1;
+
+ // Gecko codes
+
+ if (position == 0)
+ return new CheatItem(CheatItem.TYPE_HEADER, R.string.cheats_header_gecko);
+ position -= 1;
+
+ ArrayList<GeckoCheat> geckoCheats = mViewModel.getGeckoCheats();
+ if (position < geckoCheats.size())
+ return new CheatItem(geckoCheats.get(position));
+ position -= geckoCheats.size();
+
+ if (position == 0)
+ return new CheatItem(CheatItem.TYPE_ACTION, R.string.cheats_add_gecko);
+ position -= 1;
+
+ if (position == 0)
+ return new CheatItem(CheatItem.TYPE_ACTION, R.string.cheats_download_gecko);
+
+ throw new IndexOutOfBoundsException();
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/HeaderViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/HeaderViewHolder.java
new file mode 100644
index 0000000000..57c0edebb0
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/HeaderViewHolder.java
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.cheats.ui;
+
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+
+import org.dolphinemu.dolphinemu.R;
+import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel;
+
+public class HeaderViewHolder extends CheatItemViewHolder
+{
+ private TextView mHeaderName;
+
+ public HeaderViewHolder(@NonNull View itemView)
+ {
+ super(itemView);
+
+ mHeaderName = itemView.findViewById(R.id.text_header_name);
+ }
+
+ public void bind(CheatsActivity activity, CheatItem item, int position)
+ {
+ mHeaderName.setText(item.getString());
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.java
index bd71683c1f..480f69937f 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.java
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.java
@@ -14,6 +14,7 @@ public enum BooleanSetting implements AbstractBooleanSetting
MAIN_FASTMEM(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "Fastmem", true),
MAIN_CPU_THREAD(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "CPUThread", true),
MAIN_SYNC_ON_SKIP_IDLE(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "SyncOnSkipIdle", true),
+ MAIN_ENABLE_CHEATS(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "EnableCheats", false),
MAIN_OVERRIDE_REGION_SETTINGS(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE,
"OverrideRegionSettings", false),
MAIN_AUDIO_STRETCH(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "AudioStretch", false),
@@ -198,6 +199,7 @@ public enum BooleanSetting implements AbstractBooleanSetting
private static final BooleanSetting[] NOT_RUNTIME_EDITABLE_ARRAY = new BooleanSetting[]{
MAIN_DSP_HLE,
MAIN_CPU_THREAD,
+ MAIN_ENABLE_CHEATS,
MAIN_OVERRIDE_REGION_SETTINGS,
MAIN_WII_SD_CARD, // Can actually be changed, but specific code is required
MAIN_DSP_JIT
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java
index 85e26ef6ca..753dddf522 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java
@@ -18,9 +18,7 @@ import androidx.recyclerview.widget.RecyclerView;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.dialogs.MotionAlertDialog;
-import org.dolphinemu.dolphinemu.features.settings.model.AdHocBooleanSetting;
import org.dolphinemu.dolphinemu.features.settings.model.Settings;
-import org.dolphinemu.dolphinemu.features.settings.model.StringSetting;
import org.dolphinemu.dolphinemu.features.settings.model.view.CheckBoxSetting;
import org.dolphinemu.dolphinemu.features.settings.model.view.FilePicker;
import org.dolphinemu.dolphinemu.features.settings.model.view.FloatSliderSetting;
@@ -43,7 +41,6 @@ import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SettingViewHold
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SingleChoiceViewHolder;
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SliderViewHolder;
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SubmenuViewHolder;
-import org.dolphinemu.dolphinemu.ui.main.MainPresenter;
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization;
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper;
import org.dolphinemu.dolphinemu.utils.Log;
@@ -51,10 +48,7 @@ import org.dolphinemu.dolphinemu.utils.Log;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
-import java.security.InvalidParameterException;
import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.Map;
public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolder>
implements DialogInterface.OnClickListener, SeekBar.OnSeekBarChangeListener
@@ -87,7 +81,7 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
switch (viewType)
{
case SettingsItem.TYPE_HEADER:
- view = inflater.inflate(R.layout.list_item_settings_header, parent, false);
+ view = inflater.inflate(R.layout.list_item_header, parent, false);
return new HeaderViewHolder(view, this);
case SettingsItem.TYPE_CHECKBOX:
@@ -105,7 +99,7 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
return new SliderViewHolder(view, this, mContext);
case SettingsItem.TYPE_SUBMENU:
- view = inflater.inflate(R.layout.list_item_setting_submenu, parent, false);
+ view = inflater.inflate(R.layout.list_item_submenu, parent, false);
return new SubmenuViewHolder(view, this);
case SettingsItem.TYPE_INPUT_BINDING:
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.java
index 0450a7cedf..d91faa775e 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.java
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.java
@@ -258,6 +258,8 @@ public final class SettingsFragmentPresenter
{
sl.add(new CheckBoxSetting(mContext, BooleanSetting.MAIN_CPU_THREAD, R.string.dual_core,
R.string.dual_core_description));
+ sl.add(new CheckBoxSetting(mContext, BooleanSetting.MAIN_ENABLE_CHEATS, R.string.enable_cheats,
+ 0));
sl.add(new CheckBoxSetting(mContext, BooleanSetting.MAIN_OVERRIDE_REGION_SETTINGS,
R.string.override_region_settings, 0));
sl.add(new CheckBoxSetting(mContext, BooleanSetting.MAIN_AUTO_DISC_CHANGE,
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/TwoPaneOnBackPressedCallback.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/TwoPaneOnBackPressedCallback.java
new file mode 100644
index 0000000000..a6a0da5f97
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/TwoPaneOnBackPressedCallback.java
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.ui;
+
+import android.view.View;
+
+import androidx.activity.OnBackPressedCallback;
+import androidx.annotation.NonNull;
+import androidx.slidingpanelayout.widget.SlidingPaneLayout;
+
+public class TwoPaneOnBackPressedCallback extends OnBackPressedCallback
+ implements SlidingPaneLayout.PanelSlideListener
+{
+ private final SlidingPaneLayout mSlidingPaneLayout;
+
+ public TwoPaneOnBackPressedCallback(@NonNull SlidingPaneLayout slidingPaneLayout)
+ {
+ super(slidingPaneLayout.isSlideable() && slidingPaneLayout.isOpen());
+ mSlidingPaneLayout = slidingPaneLayout;
+ slidingPaneLayout.addPanelSlideListener(this);
+ }
+
+ @Override
+ public void handleOnBackPressed()
+ {
+ mSlidingPaneLayout.close();
+ }
+
+ @Override
+ public void onPanelSlide(@NonNull View panel, float slideOffset)
+ {
+ }
+
+ @Override
+ public void onPanelOpened(@NonNull View panel)
+ {
+ setEnabled(true);
+ }
+
+ @Override
+ public void onPanelClosed(@NonNull View panel)
+ {
+ setEnabled(false);
+ }
+}
+