From 53ae1a07252d628f33543e1a19427d4848cc7393 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Wed, 11 Aug 2021 17:17:30 +0200 Subject: [PATCH] Android: Add Gecko code downloading --- .../dialogs/GamePropertiesDialog.java | 11 ++-- .../cheats/model/CheatsViewModel.java | 33 ++++++++++++ .../features/cheats/model/GeckoCheat.java | 12 +++++ .../features/cheats/ui/ActionViewHolder.java | 11 +++- .../cheats/ui/CheatItemViewHolder.java | 2 +- .../features/cheats/ui/CheatViewHolder.java | 5 +- .../features/cheats/ui/CheatsActivity.java | 52 ++++++++++++++++++- .../features/cheats/ui/CheatsAdapter.java | 17 +++++- .../features/cheats/ui/HeaderViewHolder.java | 2 +- .../app/src/main/res/values/strings.xml | 5 ++ Source/Android/jni/Cheats/GeckoCheat.cpp | 29 +++++++++++ Source/Core/Core/GeckoCodeConfig.cpp | 7 ++- Source/Core/Core/GeckoCodeConfig.h | 3 +- 13 files changed, 173 insertions(+), 16 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 15e6c8bc31..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 @@ -29,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"; @@ -40,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()); @@ -54,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 = @@ -93,7 +96,7 @@ public class GamePropertiesDialog extends DialogFragment SettingsActivity.launch(getContext(), MenuTag.SETTINGS, gameId, revision, isWii)); itemsBuilder.add(R.string.properties_edit_cheats, (dialog, i) -> - CheatsActivity.launch(getContext(), gameId, revision, isWii)); + 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/CheatsViewModel.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/model/CheatsViewModel.java index 750c921c94..0238628319 100644 --- 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 @@ -21,6 +21,7 @@ public class CheatsViewModel extends ViewModel private final MutableLiveData mCheatAddedEvent = new MutableLiveData<>(null); private final MutableLiveData mCheatChangedEvent = new MutableLiveData<>(null); private final MutableLiveData mCheatDeletedEvent = new MutableLiveData<>(null); + private final MutableLiveData mGeckoCheatsDownloadedEvent = new MutableLiveData<>(null); private final MutableLiveData mOpenDetailsViewEvent = new MutableLiveData<>(false); private ArrayList mPatchCheats; @@ -236,6 +237,38 @@ public class CheatsViewModel extends ViewModel 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 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 getOpenDetailsViewEvent() { return mOpenDetailsViewEvent; 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 index 19573723b6..4397a95d81 100644 --- 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 @@ -4,6 +4,7 @@ package org.dolphinemu.dolphinemu.features.cheats.model; import androidx.annotation.Keep; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; public class GeckoCheat extends AbstractCheat { @@ -26,6 +27,12 @@ public class GeckoCheat extends AbstractCheat 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; @@ -52,6 +59,8 @@ public class GeckoCheat extends AbstractCheat 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); @@ -63,4 +72,7 @@ public class GeckoCheat extends AbstractCheat 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/ui/ActionViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/ActionViewHolder.java index 72380f8ab1..3e99384fdd 100644 --- 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 @@ -6,6 +6,7 @@ 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; @@ -17,6 +18,7 @@ public class ActionViewHolder extends CheatItemViewHolder implements View.OnClic { private final TextView mName; + private CheatsActivity mActivity; private CheatsViewModel mViewModel; private int mString; private int mPosition; @@ -30,9 +32,10 @@ public class ActionViewHolder extends CheatItemViewHolder implements View.OnClic itemView.setOnClickListener(this); } - public void bind(CheatsViewModel viewModel, CheatItem item, int position) + public void bind(CheatsActivity activity, CheatItem item, int position) { - mViewModel = viewModel; + mActivity = activity; + mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class); mString = item.getString(); mPosition = position; @@ -56,5 +59,9 @@ public class ActionViewHolder extends CheatItemViewHolder implements View.OnClic 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/CheatItemViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatItemViewHolder.java index ca07d3a871..b4e38f4c1b 100644 --- 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 @@ -16,5 +16,5 @@ public abstract class CheatItemViewHolder extends RecyclerView.ViewHolder super(itemView); } - public abstract void bind(CheatsViewModel viewModel, CheatItem item, int position); + 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/CheatViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatViewHolder.java index 3f56f480cd..58e391fb01 100644 --- 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 @@ -8,6 +8,7 @@ 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; @@ -34,11 +35,11 @@ public class CheatViewHolder extends CheatItemViewHolder mCheckbox = itemView.findViewById(R.id.checkbox); } - public void bind(CheatsViewModel viewModel, CheatItem item, int position) + public void bind(CheatsActivity activity, CheatItem item, int position) { mCheckbox.setOnCheckedChangeListener(null); - mViewModel = viewModel; + mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class); mCheat = item.getCheat(); mPosition = position; 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 index 42300e2b8d..e1210a86f6 100644 --- 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 @@ -9,6 +9,7 @@ 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; @@ -17,6 +18,7 @@ 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; @@ -25,10 +27,12 @@ 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; @@ -40,10 +44,12 @@ public class CheatsActivity extends AppCompatActivity private View mCheatListLastFocus; private View mCheatDetailsLastFocus; - public static void launch(Context context, String gameId, int revision, boolean isWii) + 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); @@ -58,6 +64,7 @@ public class CheatsActivity extends AppCompatActivity 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); @@ -161,6 +168,49 @@ public class CheatsActivity extends AppCompatActivity 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) { 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 index ed230f0513..5994feab2a 100644 --- 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 @@ -45,6 +45,16 @@ public class CheatsAdapter extends RecyclerView.Adapter 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 @@ -75,14 +85,14 @@ public class CheatsAdapter extends RecyclerView.Adapter @Override public void onBindViewHolder(@NonNull CheatItemViewHolder holder, int position) { - holder.bind(mViewModel, getItemAt(position), position); + holder.bind(mActivity, getItemAt(position), position); } @Override public int getItemCount() { return mViewModel.getARCheats().size() + mViewModel.getGeckoCheats().size() + - mViewModel.getPatchCheats().size() + 6; + mViewModel.getPatchCheats().size() + 7; } @Override @@ -144,6 +154,9 @@ public class CheatsAdapter extends RecyclerView.Adapter 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 index 29da69c9da..57c0edebb0 100644 --- 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 @@ -21,7 +21,7 @@ public class HeaderViewHolder extends CheatItemViewHolder mHeaderName = itemView.findViewById(R.id.text_header_name); } - public void bind(CheatsViewModel viewModel, CheatItem item, int position) + public void bind(CheatsActivity activity, CheatItem item, int position) { mHeaderName.setText(item.getString()); } diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 7191802de8..2cb4b45643 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -395,6 +395,7 @@ Add New AR Code Add New Gecko Code Add New Patch + Download Gecko Codes Name Creator Notes @@ -406,6 +407,10 @@ Code can\'t be empty Error on line %1$d Lines must either be all encrypted or all decrypted + Downloading... + Failed to download codes. + File contained no codes. + Downloaded %1$d codes. (added %2$d) Dolphin\'s cheat system is currently disabled. Settings diff --git a/Source/Android/jni/Cheats/GeckoCheat.cpp b/Source/Android/jni/Cheats/GeckoCheat.cpp index 1765715581..15038984c2 100644 --- a/Source/Android/jni/Cheats/GeckoCheat.cpp +++ b/Source/Android/jni/Cheats/GeckoCheat.cpp @@ -92,6 +92,13 @@ Java_org_dolphinemu_dolphinemu_features_cheats_model_GeckoCheat_getEnabled(JNIEn return static_cast(GetPointer(env, obj)->enabled); } +JNIEXPORT jboolean JNICALL +Java_org_dolphinemu_dolphinemu_features_cheats_model_GeckoCheat_equalsImpl(JNIEnv* env, jobject obj, + jobject other) +{ + return *GetPointer(env, obj) == *GetPointer(env, other); +} + JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_features_cheats_model_GeckoCheat_trySetImpl( JNIEnv* env, jobject obj, jstring name, jstring creator, jstring notes, jstring code_string) { @@ -180,4 +187,26 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_cheats_model_Geck Gecko::SaveCodes(game_ini_local, vector); game_ini_local.Save(ini_path); } + +JNIEXPORT jobjectArray JNICALL +Java_org_dolphinemu_dolphinemu_features_cheats_model_GeckoCheat_downloadCodes(JNIEnv* env, jclass, + jstring jGameTdbId) +{ + const std::string gametdb_id = GetJString(env, jGameTdbId); + + bool success = true; + const std::vector codes = Gecko::DownloadCodes(gametdb_id, &success, false); + + if (!success) + return nullptr; + + const jobjectArray array = + env->NewObjectArray(static_cast(codes.size()), IDCache::GetGeckoCheatClass(), nullptr); + + jsize i = 0; + for (const Gecko::GeckoCode& code : codes) + env->SetObjectArrayElement(array, i++, GeckoCheatToJava(env, code)); + + return array; +} } diff --git a/Source/Core/Core/GeckoCodeConfig.cpp b/Source/Core/Core/GeckoCodeConfig.cpp index fd5d416f2e..ebb1f78160 100644 --- a/Source/Core/Core/GeckoCodeConfig.cpp +++ b/Source/Core/Core/GeckoCodeConfig.cpp @@ -17,10 +17,13 @@ namespace Gecko { -std::vector DownloadCodes(std::string gametdb_id, bool* succeeded) +std::vector DownloadCodes(std::string gametdb_id, bool* succeeded, bool use_https) { + // TODO: Fix https://bugs.dolphin-emu.org/issues/11772 so we don't need this workaround + const std::string protocol = use_https ? "https://" : "http://"; + // codes.rc24.xyz is a mirror of the now defunct geckocodes.org. - std::string endpoint{"https://codes.rc24.xyz/txt.php?txt=" + gametdb_id}; + std::string endpoint{protocol + "codes.rc24.xyz/txt.php?txt=" + gametdb_id}; Common::HttpRequest http; // The server always redirects once to the same location. diff --git a/Source/Core/Core/GeckoCodeConfig.h b/Source/Core/Core/GeckoCodeConfig.h index 1b43539c0c..2497ab603e 100644 --- a/Source/Core/Core/GeckoCodeConfig.h +++ b/Source/Core/Core/GeckoCodeConfig.h @@ -14,7 +14,8 @@ class IniFile; namespace Gecko { std::vector LoadCodes(const IniFile& globalIni, const IniFile& localIni); -std::vector DownloadCodes(std::string gametdb_id, bool* succeeded); +std::vector DownloadCodes(std::string gametdb_id, bool* succeeded, + bool use_https = true); void SaveCodes(IniFile& inifile, const std::vector& gcodes); std::optional DeserializeLine(const std::string& line);