From 2e47afd48e94811f069cd7c50ae713a65b5b738a Mon Sep 17 00:00:00 2001
From: JosJuice <josjuice@gmail.com>
Date: Sun, 7 May 2023 16:01:34 +0200
Subject: [PATCH] android: Open cheats by long pressing game in game list
 (#6491)

---
 .../org/citra/citra_emu/NativeLibrary.java    |  7 ++++
 .../activities/EmulationActivity.java         |  2 +-
 .../citra/citra_emu/adapters/GameAdapter.java | 40 ++++++++++++++++---
 .../features/cheats/model/CheatEngine.java    | 25 +++++++++---
 .../cheats/model/CheatsViewModel.java         | 18 ++++++---
 .../features/cheats/ui/CheatsActivity.java    |  9 ++++-
 .../app/src/main/jni/cheats/cheat_engine.cpp  | 37 ++++++++++++-----
 src/android/app/src/main/jni/id_cache.cpp     | 12 ++++++
 src/android/app/src/main/jni/id_cache.h       |  2 +
 src/android/app/src/main/jni/native.cpp       | 20 ++++++++++
 .../app/src/main/res/values/strings.xml       |  4 ++
 11 files changed, 145 insertions(+), 31 deletions(-)

diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java
index 08041f790..4af8b5c8f 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java
+++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java
@@ -128,6 +128,8 @@ public final class NativeLibrary {
 
     public static native void InitGameIni(String gameID);
 
+    public static native long GetTitleId(String filename);
+
     public static native String GetGitRevision();
 
     /**
@@ -186,6 +188,11 @@ public final class NativeLibrary {
      */
     public static native boolean IsRunning();
 
+    /**
+     * Returns the title ID of the currently running title, or 0 on failure.
+     */
+    public static native long GetRunningTitleId();
+
     /**
      * Returns the performance stats for the current game
      **/
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java
index 6b4787082..88bb0f3a5 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java
+++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java
@@ -491,7 +491,7 @@ public final class EmulationActivity extends AppCompatActivity {
                 break;
 
             case MENU_ACTION_OPEN_CHEATS:
-                CheatsActivity.launch(this);
+                CheatsActivity.launch(this, NativeLibrary.GetRunningTitleId());
                 break;
 
             case MENU_ACTION_CLOSE_GAME:
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.java b/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.java
index 1facecaa3..1c3cad9b1 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.java
+++ b/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.java
@@ -1,5 +1,6 @@
 package org.citra.citra_emu.adapters;
 
+import android.content.Context;
 import android.database.Cursor;
 import android.database.DataSetObserver;
 import android.os.Build;
@@ -14,10 +15,13 @@ import androidx.fragment.app.FragmentActivity;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.google.android.material.color.MaterialColors;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 
 import org.citra.citra_emu.CitraApplication;
+import org.citra.citra_emu.NativeLibrary;
 import org.citra.citra_emu.R;
 import org.citra.citra_emu.activities.EmulationActivity;
+import org.citra.citra_emu.features.cheats.ui.CheatsActivity;
 import org.citra.citra_emu.model.GameDatabase;
 import org.citra.citra_emu.utils.FileUtil;
 import org.citra.citra_emu.utils.Log;
@@ -31,8 +35,7 @@ import java.util.stream.Stream;
  * ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly)
  * large dataset.
  */
-public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> implements
-        View.OnClickListener {
+public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> {
     private Cursor mCursor;
     private GameDataSetObserver mObserver;
 
@@ -61,7 +64,8 @@ public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> impl
         View gameCard = LayoutInflater.from(parent.getContext())
                 .inflate(R.layout.card_game, parent, false);
 
-        gameCard.setOnClickListener(this);
+        gameCard.setOnClickListener(this::onClick);
+        gameCard.setOnLongClickListener(this::onLongClick);
 
         // Use that view to create a ViewHolder.
         return new GameViewHolder(gameCard);
@@ -193,10 +197,9 @@ public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> impl
     /**
      * Launches the game that was clicked on.
      *
-     * @param view The card representing the game the user wants to play.
+     * @param view The view representing the game the user wants to play.
      */
-    @Override
-    public void onClick(View view) {
+    private void onClick(View view) {
         // Double-click prevention, using threshold of 1000 ms
         if (SystemClock.elapsedRealtime() - mLastClickTime < 1000) {
             return;
@@ -208,6 +211,31 @@ public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> impl
         EmulationActivity.launch((FragmentActivity) view.getContext(), holder.path, holder.title);
     }
 
+    /**
+     * Opens the cheats settings for the game that was clicked on.
+     *
+     * @param view The view representing the game the user wants to play.
+     */
+    private boolean onLongClick(View view) {
+        Context context = view.getContext();
+        GameViewHolder holder = (GameViewHolder) view.getTag();
+
+        final long titleId = NativeLibrary.GetTitleId(holder.path);
+
+        if (titleId == 0) {
+            new MaterialAlertDialogBuilder(context)
+                    .setIcon(R.mipmap.ic_launcher)
+                    .setTitle(R.string.properties)
+                    .setMessage(R.string.properties_not_loaded)
+                    .setPositiveButton(android.R.string.ok, null)
+                    .show();
+        } else {
+            CheatsActivity.launch(context, titleId);
+        }
+
+        return true;
+    }
+
     private boolean isValidGame(String path) {
         return Stream.of(
                 ".rar", ".zip", ".7z", ".torrent", ".tar", ".gz").noneMatch(suffix -> path.toLowerCase().endsWith(suffix));
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatEngine.java b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatEngine.java
index 5748162bb..a1e88a3d3 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatEngine.java
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatEngine.java
@@ -1,13 +1,28 @@
 package org.citra.citra_emu.features.cheats.model;
 
+import androidx.annotation.Keep;
+
 public class CheatEngine {
-    public static native Cheat[] getCheats();
+    @Keep
+    private final long mPointer;
 
-    public static native void addCheat(Cheat cheat);
+    @Keep
+    public CheatEngine(long titleId) {
+        mPointer = initialize(titleId);
+    }
 
-    public static native void removeCheat(int index);
+    private static native long initialize(long titleId);
 
-    public static native void updateCheat(int index, Cheat newCheat);
+    @Override
+    protected native void finalize();
 
-    public static native void saveCheatFile();
+    public native Cheat[] getCheats();
+
+    public native void addCheat(Cheat cheat);
+
+    public native void removeCheat(int index);
+
+    public native void updateCheat(int index, Cheat newCheat);
+
+    public native void saveCheatFile();
 }
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatsViewModel.java b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatsViewModel.java
index cb4788cb8..dbeb34c21 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatsViewModel.java
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatsViewModel.java
@@ -19,11 +19,17 @@ public class CheatsViewModel extends ViewModel {
     private final MutableLiveData<Integer> mCheatDeletedEvent = new MutableLiveData<>(null);
     private final MutableLiveData<Boolean> mOpenDetailsViewEvent = new MutableLiveData<>(false);
 
+    private CheatEngine mCheatEngine;
     private Cheat[] mCheats;
     private boolean mCheatsNeedSaving = false;
 
-    public void load() {
-        mCheats = CheatEngine.getCheats();
+    public void initialize(long titleId) {
+        mCheatEngine = new CheatEngine(titleId);
+        load();
+    }
+
+    private void load() {
+        mCheats = mCheatEngine.getCheats();
 
         for (int i = 0; i < mCheats.length; i++) {
             int position = i;
@@ -36,7 +42,7 @@ public class CheatsViewModel extends ViewModel {
 
     public void saveIfNeeded() {
         if (mCheatsNeedSaving) {
-            CheatEngine.saveCheatFile();
+            mCheatEngine.saveCheatFile();
             mCheatsNeedSaving = false;
         }
     }
@@ -106,7 +112,7 @@ public class CheatsViewModel extends ViewModel {
 
         int position = mCheats.length;
 
-        CheatEngine.addCheat(cheat);
+        mCheatEngine.addCheat(cheat);
 
         mCheatsNeedSaving = true;
         load();
@@ -132,7 +138,7 @@ public class CheatsViewModel extends ViewModel {
     }
 
     public void updateSelectedCheat(Cheat newCheat) {
-        CheatEngine.updateCheat(mSelectedCheatPosition, newCheat);
+        mCheatEngine.updateCheat(mSelectedCheatPosition, newCheat);
 
         mCheatsNeedSaving = true;
         load();
@@ -162,7 +168,7 @@ public class CheatsViewModel extends ViewModel {
 
         setSelectedCheat(null, -1);
 
-        CheatEngine.removeCheat(position);
+        mCheatEngine.removeCheat(position);
 
         mCheatsNeedSaving = true;
         load();
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsActivity.java
index 5df4bc83d..45d0daf5b 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsActivity.java
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsActivity.java
@@ -32,6 +32,8 @@ import java.util.List;
 
 public class CheatsActivity extends AppCompatActivity
         implements SlidingPaneLayout.PanelSlideListener {
+    private static String ARG_TITLE_ID = "title_id";
+
     private CheatsViewModel mViewModel;
 
     private SlidingPaneLayout mSlidingPaneLayout;
@@ -41,8 +43,9 @@ public class CheatsActivity extends AppCompatActivity
     private View mCheatListLastFocus;
     private View mCheatDetailsLastFocus;
 
-    public static void launch(Context context) {
+    public static void launch(Context context, long titleId) {
         Intent intent = new Intent(context, CheatsActivity.class);
+        intent.putExtra(ARG_TITLE_ID, titleId);
         context.startActivity(intent);
     }
 
@@ -54,8 +57,10 @@ public class CheatsActivity extends AppCompatActivity
 
         WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
 
+        long titleId = getIntent().getLongExtra(ARG_TITLE_ID, -1);
+
         mViewModel = new ViewModelProvider(this).get(CheatsViewModel.class);
-        mViewModel.load();
+        mViewModel.initialize(titleId);
 
         setContentView(R.layout.activity_cheats);
 
diff --git a/src/android/app/src/main/jni/cheats/cheat_engine.cpp b/src/android/app/src/main/jni/cheats/cheat_engine.cpp
index 61dd7354c..5010bd14f 100644
--- a/src/android/app/src/main/jni/cheats/cheat_engine.cpp
+++ b/src/android/app/src/main/jni/cheats/cheat_engine.cpp
@@ -15,9 +15,24 @@
 
 extern "C" {
 
+static Cheats::CheatEngine* GetPointer(JNIEnv* env, jobject obj) {
+    return reinterpret_cast<Cheats::CheatEngine*>(
+        env->GetLongField(obj, IDCache::GetCheatEnginePointer()));
+}
+
+JNIEXPORT jlong JNICALL Java_org_citra_citra_1emu_features_cheats_model_CheatEngine_initialize(
+    JNIEnv* env, jclass, jlong title_id) {
+    return reinterpret_cast<jlong>(new Cheats::CheatEngine(title_id, Core::System::GetInstance()));
+}
+
+JNIEXPORT void JNICALL
+Java_org_citra_citra_1emu_features_cheats_model_CheatEngine_finalize(JNIEnv* env, jobject obj) {
+    delete GetPointer(env, obj);
+}
+
 JNIEXPORT jobjectArray JNICALL
-Java_org_citra_citra_1emu_features_cheats_model_CheatEngine_getCheats(JNIEnv* env, jclass) {
-    auto cheats = Core::System::GetInstance().CheatEngine().GetCheats();
+Java_org_citra_citra_1emu_features_cheats_model_CheatEngine_getCheats(JNIEnv* env, jobject obj) {
+    auto cheats = GetPointer(env, obj)->GetCheats();
 
     const jobjectArray array =
         env->NewObjectArray(static_cast<jsize>(cheats.size()), IDCache::GetCheatClass(), nullptr);
@@ -30,22 +45,22 @@ Java_org_citra_citra_1emu_features_cheats_model_CheatEngine_getCheats(JNIEnv* en
 }
 
 JNIEXPORT void JNICALL Java_org_citra_citra_1emu_features_cheats_model_CheatEngine_addCheat(
-    JNIEnv* env, jclass, jobject j_cheat) {
-    Core::System::GetInstance().CheatEngine().AddCheat(*CheatFromJava(env, j_cheat));
+    JNIEnv* env, jobject obj, jobject j_cheat) {
+    GetPointer(env, obj)->AddCheat(*CheatFromJava(env, j_cheat));
 }
 
 JNIEXPORT void JNICALL Java_org_citra_citra_1emu_features_cheats_model_CheatEngine_removeCheat(
-    JNIEnv* env, jclass, jint index) {
-    Core::System::GetInstance().CheatEngine().RemoveCheat(index);
+    JNIEnv* env, jobject obj, jint index) {
+    GetPointer(env, obj)->RemoveCheat(index);
 }
 
 JNIEXPORT void JNICALL Java_org_citra_citra_1emu_features_cheats_model_CheatEngine_updateCheat(
-    JNIEnv* env, jclass, jint index, jobject j_new_cheat) {
-    Core::System::GetInstance().CheatEngine().UpdateCheat(index, *CheatFromJava(env, j_new_cheat));
+    JNIEnv* env, jobject obj, jint index, jobject j_new_cheat) {
+    GetPointer(env, obj)->UpdateCheat(index, *CheatFromJava(env, j_new_cheat));
 }
 
-JNIEXPORT void JNICALL
-Java_org_citra_citra_1emu_features_cheats_model_CheatEngine_saveCheatFile(JNIEnv* env, jclass) {
-    Core::System::GetInstance().CheatEngine().SaveCheatFile();
+JNIEXPORT void JNICALL Java_org_citra_citra_1emu_features_cheats_model_CheatEngine_saveCheatFile(
+    JNIEnv* env, jobject obj) {
+    GetPointer(env, obj)->SaveCheatFile();
 }
 }
diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp
index f90c046c2..c34b01917 100644
--- a/src/android/app/src/main/jni/id_cache.cpp
+++ b/src/android/app/src/main/jni/id_cache.cpp
@@ -40,6 +40,8 @@ static jclass s_cheat_class;
 static jfieldID s_cheat_pointer;
 static jmethodID s_cheat_constructor;
 
+static jfieldID s_cheat_engine_pointer;
+
 static jfieldID s_game_info_pointer;
 
 static std::unordered_map<VideoCore::LoadCallbackStage, jobject> s_java_load_callback_stages;
@@ -137,6 +139,10 @@ jmethodID GetCheatConstructor() {
     return s_cheat_constructor;
 }
 
+jfieldID GetCheatEnginePointer() {
+    return s_cheat_engine_pointer;
+}
+
 jfieldID GetGameInfoPointer() {
     return s_game_info_pointer;
 }
@@ -211,6 +217,12 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
     s_cheat_constructor = env->GetMethodID(cheat_class, "<init>", "(J)V");
     env->DeleteLocalRef(cheat_class);
 
+    // Initialize CheatEngine
+    const jclass cheat_engine_class =
+        env->FindClass("org/citra/citra_emu/features/cheats/model/CheatEngine");
+    s_cheat_engine_pointer = env->GetFieldID(cheat_engine_class, "mPointer", "J");
+    env->DeleteLocalRef(cheat_engine_class);
+
     // Initialize GameInfo
     const jclass game_info_class = env->FindClass("org/citra/citra_emu/model/GameInfo");
     s_game_info_pointer = env->GetFieldID(game_info_class, "mPointer", "J");
diff --git a/src/android/app/src/main/jni/id_cache.h b/src/android/app/src/main/jni/id_cache.h
index 0fd687666..e57496fae 100644
--- a/src/android/app/src/main/jni/id_cache.h
+++ b/src/android/app/src/main/jni/id_cache.h
@@ -34,6 +34,8 @@ jclass GetCheatClass();
 jfieldID GetCheatPointer();
 jmethodID GetCheatConstructor();
 
+jfieldID GetCheatEnginePointer();
+
 jfieldID GetGameInfoPointer();
 
 jobject GetJavaLoadCallbackStage(VideoCore::LoadCallbackStage stage);
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index 48d37666a..0c1620ec6 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -24,6 +24,7 @@
 #include "core/frontend/camera/factory.h"
 #include "core/hle/service/am/am.h"
 #include "core/hle/service/nfc/nfc.h"
+#include "core/loader/loader.h"
 #include "core/savestate.h"
 #include "jni/android_common/android_common.h"
 #include "jni/applets/mii_selector.h"
@@ -379,6 +380,13 @@ jboolean Java_org_citra_citra_1emu_NativeLibrary_IsRunning(JNIEnv* env,
     return static_cast<jboolean>(!stop_run);
 }
 
+jlong Java_org_citra_citra_1emu_NativeLibrary_GetRunningTitleId(JNIEnv* env,
+                                                                [[maybe_unused]] jclass clazz) {
+    u64 title_id{};
+    Core::System::GetInstance().GetAppLoader().ReadProgramId(title_id);
+    return static_cast<jlong>(title_id);
+}
+
 jboolean Java_org_citra_citra_1emu_NativeLibrary_onGamePadEvent(JNIEnv* env,
                                                                 [[maybe_unused]] jclass clazz,
                                                                 jstring j_device, jint j_button,
@@ -435,6 +443,18 @@ void Java_org_citra_citra_1emu_NativeLibrary_onTouchMoved(JNIEnv* env,
     window->OnTouchMoved((int)x, (int)y);
 }
 
+jlong Java_org_citra_citra_1emu_NativeLibrary_GetTitleId(JNIEnv* env, [[maybe_unused]] jclass clazz,
+                                                         jstring j_filename) {
+    std::string filepath = GetJString(env, j_filename);
+    const auto loader = Loader::GetLoader(filepath);
+
+    u64 title_id{};
+    if (loader) {
+        loader->ReadProgramId(title_id);
+    }
+    return static_cast<jlong>(title_id);
+}
+
 jstring Java_org_citra_citra_1emu_NativeLibrary_GetGitRevision(JNIEnv* env,
                                                                [[maybe_unused]] jclass clazz) {
     return nullptr;
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index ba47472c6..03c403ece 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -146,6 +146,10 @@
     <string name="select_game_folder">Select Game Folder</string>
     <string name="install_cia_title">Install CIA</string>
 
+    <!-- Game Properties -->
+    <string name="properties">Properties</string>
+    <string name="properties_not_loaded">The game properties could not be loaded.</string>
+
     <!-- Preferences Screen -->
     <string name="preferences_settings">Settings</string>
     <string name="preferences_premium">Premium</string>