summaryrefslogtreecommitdiff
path: root/Source/Android
diff options
context:
space:
mode:
authorJosJuice <josjuice@gmail.com>2025-11-22 17:00:03 +0100
committerGitHub <noreply@github.com>2025-11-22 17:00:03 +0100
commitf4f7424a9f059a25ec09b22a280d03fcf0e751eb (patch)
treeda9b25179aad7e305bada139bb2062951e68542d /Source/Android
parente526c1ffde96e619bc764167f6fc337db74551ed (diff)
parentddced1a070848d935bd1d140eed549a4cae89566 (diff)
Merge pull request #13583 from LillyJadeKatrin/retroachievements-android
Android Support for RetroAchievements
Diffstat (limited to 'Source/Android')
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/AchievementModel.kt29
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt43
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/Settings.kt2
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt12
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/LoginDialog.kt63
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.kt1
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt1
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt114
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt3
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.kt6
-rw-r--r--Source/Android/app/src/main/res/layout/dialog_login.xml110
-rw-r--r--Source/Android/app/src/main/res/values/strings.xml15
-rw-r--r--Source/Android/jni/AchievementAdapter.cpp58
-rw-r--r--Source/Android/jni/CMakeLists.txt1
-rw-r--r--Source/Android/jni/Config/NativeConfig.cpp4
-rw-r--r--Source/Android/jni/MainAndroid.cpp3
16 files changed, 462 insertions, 3 deletions
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/AchievementModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/AchievementModel.kt
new file mode 100644
index 0000000000..1ea53ddbec
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/AchievementModel.kt
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.settings.model
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+object AchievementModel {
+ @JvmStatic
+ external fun init()
+
+ suspend fun asyncLogin(password: String): Boolean {
+ return withContext(Dispatchers.IO) {
+ login(password)
+ }
+ }
+
+ @JvmStatic
+ private external fun login(password: String): Boolean
+
+ @JvmStatic
+ external fun logout()
+
+ @JvmStatic
+ external fun isHardcoreModeActive(): Boolean
+
+ @JvmStatic
+ external fun shutdown()
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt
index 0b6945ff13..1f8ef6e94b 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt
@@ -883,6 +883,42 @@ enum class BooleanSetting(
Settings.SECTION_LOGGER_OPTIONS,
"WriteToFile",
false
+ ),
+ ACHIEVEMENTS_ENABLED(
+ Settings.FILE_ACHIEVEMENTS,
+ Settings.SECTION_ACHIEVEMENTS,
+ "Enabled",
+ false
+ ),
+ ACHIEVEMENTS_HARDCORE_ENABLED(
+ Settings.FILE_ACHIEVEMENTS,
+ Settings.SECTION_ACHIEVEMENTS,
+ "HardcoreEnabled",
+ false
+ ),
+ ACHIEVEMENTS_UNOFFICIAL_ENABLED(
+ Settings.FILE_ACHIEVEMENTS,
+ Settings.SECTION_ACHIEVEMENTS,
+ "UnofficialEnabled",
+ false
+ ),
+ ACHIEVEMENTS_ENCORE_ENABLED(
+ Settings.FILE_ACHIEVEMENTS,
+ Settings.SECTION_ACHIEVEMENTS,
+ "EncoreEnabled",
+ false
+ ),
+ ACHIEVEMENTS_SPECTATOR_ENABLED(
+ Settings.FILE_ACHIEVEMENTS,
+ Settings.SECTION_ACHIEVEMENTS,
+ "SpectatorEnabled",
+ false
+ ),
+ ACHIEVEMENTS_PROGRESS_ENABLED(
+ Settings.FILE_ACHIEVEMENTS,
+ Settings.SECTION_ACHIEVEMENTS,
+ "ProgressEnabled",
+ true
);
override val isOverridden: Boolean
@@ -943,7 +979,12 @@ enum class BooleanSetting(
MAIN_TIME_TRACKING,
MAIN_EMULATE_SKYLANDER_PORTAL,
MAIN_EMULATE_INFINITY_BASE,
- MAIN_EMULATE_WII_SPEAK
+ MAIN_EMULATE_WII_SPEAK,
+ ACHIEVEMENTS_ENABLED,
+ ACHIEVEMENTS_HARDCORE_ENABLED,
+ ACHIEVEMENTS_UNOFFICIAL_ENABLED,
+ ACHIEVEMENTS_ENCORE_ENABLED,
+ ACHIEVEMENTS_SPECTATOR_ENABLED
)
private val NOT_RUNTIME_EDITABLE: Set<BooleanSetting> =
HashSet(listOf(*NOT_RUNTIME_EDITABLE_ARRAY))
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/Settings.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/Settings.kt
index 32ad11b3e1..9f91a4244c 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/Settings.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/Settings.kt
@@ -104,6 +104,7 @@ class Settings : Closeable {
const val FILE_GFX = "GFX"
const val FILE_LOGGER = "Logger"
const val FILE_WIIMOTE = "WiimoteNew"
+ const val FILE_ACHIEVEMENTS = "RetroAchievements"
const val FILE_GAME_SETTINGS_ONLY = "GameSettingsOnly"
const val SECTION_INI_ANDROID = "Android"
const val SECTION_INI_ANDROID_OVERLAY_BUTTONS = "AndroidOverlayButtons"
@@ -122,5 +123,6 @@ class Settings : Closeable {
const val SECTION_EMULATED_USB_DEVICES = "EmulatedUSBDevices"
const val SECTION_STEREOSCOPY = "Stereoscopy"
const val SECTION_ANALYTICS = "Analytics"
+ const val SECTION_ACHIEVEMENTS = "Achievements"
}
}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt
index b150c845e1..29640aa92f 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt
@@ -80,6 +80,18 @@ enum class StringSetting(
Settings.SECTION_GFX_SETTINGS,
"DriverLibName",
""
+ ),
+ ACHIEVEMENTS_USERNAME(
+ Settings.FILE_ACHIEVEMENTS,
+ Settings.SECTION_ACHIEVEMENTS,
+ "Username",
+ ""
+ ),
+ ACHIEVEMENTS_API_TOKEN(
+ Settings.FILE_ACHIEVEMENTS,
+ Settings.SECTION_ACHIEVEMENTS,
+ "ApiToken",
+ ""
);
override val isOverridden: Boolean
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/LoginDialog.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/LoginDialog.kt
new file mode 100644
index 0000000000..9f1d3efd5f
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/LoginDialog.kt
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.settings.ui
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.DialogFragment
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.launch
+import org.dolphinemu.dolphinemu.databinding.DialogLoginBinding
+import org.dolphinemu.dolphinemu.dialogs.AlertMessage
+import org.dolphinemu.dolphinemu.features.settings.model.AchievementModel.asyncLogin
+import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig
+import org.dolphinemu.dolphinemu.features.settings.model.StringSetting
+
+class LoginDialog(val parent: SettingsFragmentPresenter) : DialogFragment() {
+ private var _binding: DialogLoginBinding? = null
+ private val binding get() = _binding!!
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = DialogLoginBinding.inflate(inflater, container, false)
+ binding.usernameInput.setText(
+ StringSetting.ACHIEVEMENTS_USERNAME.string
+ )
+ return binding.getRoot()
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ binding.buttonCancel.setOnClickListener { onCancelClicked() }
+ binding.buttonLogin.setOnClickListener { onLoginClicked() }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
+ private fun onCancelClicked() {
+ dismiss()
+ }
+
+ private fun onLoginClicked() {
+ binding.loginFailed.visibility = View.GONE
+ binding.loginInProgress.visibility = View.VISIBLE
+ StringSetting.ACHIEVEMENTS_USERNAME.setString(NativeConfig.LAYER_BASE_OR_CURRENT,
+ binding.usernameInput.text.toString())
+ lifecycleScope.launch {
+ if (asyncLogin(binding.passwordInput.text.toString())) {
+ parent.loadSettingsList()
+ dismiss()
+ } else {
+ binding.loginInProgress.visibility = View.GONE
+ binding.loginFailed.visibility = View.VISIBLE
+ }
+ }
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.kt
index 0bfcb96d6c..74556b07d9 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/MenuTag.kt
@@ -14,6 +14,7 @@ enum class MenuTag {
CONFIG_GAME_CUBE("config_gamecube"),
CONFIG_SERIALPORT1("config_serialport1"),
CONFIG_WII("config_wii"),
+ CONFIG_ACHIEVEMENTS("config_achievements"),
CONFIG_ADVANCED("config_advanced"),
CONFIG_LOG("config_log"),
DEBUG("debug"),
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt
index 2992dd3211..1289d30f61 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt
@@ -268,6 +268,7 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
titles[MenuTag.CONFIG_GAME_CUBE] = R.string.gamecube_submenu
titles[MenuTag.CONFIG_SERIALPORT1] = R.string.serialport1_submenu
titles[MenuTag.CONFIG_WII] = R.string.wii_submenu
+ titles[MenuTag.CONFIG_ACHIEVEMENTS] = R.string.achievements_submenu
titles[MenuTag.CONFIG_ADVANCED] = R.string.advanced_submenu
titles[MenuTag.DEBUG] = R.string.debug_submenu
titles[MenuTag.GRAPHICS] = R.string.graphics_settings
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt
index a47901debb..a4327b9645 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt
@@ -31,6 +31,7 @@ import org.dolphinemu.dolphinemu.features.input.ui.ProfileDialog
import org.dolphinemu.dolphinemu.features.input.ui.ProfileDialogPresenter
import org.dolphinemu.dolphinemu.features.settings.model.*
import org.dolphinemu.dolphinemu.features.settings.model.view.*
+import org.dolphinemu.dolphinemu.features.settings.model.AchievementModel.logout
import org.dolphinemu.dolphinemu.model.GpuDriverMetadata
import org.dolphinemu.dolphinemu.utils.*
import kotlin.collections.ArrayList
@@ -98,7 +99,7 @@ class SettingsFragmentPresenter(
}
}
- private fun loadSettingsList() {
+ fun loadSettingsList() {
val sl = ArrayList<SettingsItem>()
when (menuTag) {
MenuTag.SETTINGS -> addTopLevelSettings(sl)
@@ -109,6 +110,7 @@ class SettingsFragmentPresenter(
MenuTag.CONFIG_PATHS -> addPathsSettings(sl)
MenuTag.CONFIG_GAME_CUBE -> addGameCubeSettings(sl)
MenuTag.CONFIG_WII -> addWiiSettings(sl)
+ MenuTag.CONFIG_ACHIEVEMENTS -> addAchievementSettings(sl);
MenuTag.CONFIG_ADVANCED -> addAdvancedSettings(sl)
MenuTag.GRAPHICS -> addGraphicsSettings(sl)
MenuTag.CONFIG_SERIALPORT1 -> addSerialPortSubSettings(sl, serialPort1Type)
@@ -198,6 +200,7 @@ class SettingsFragmentPresenter(
sl.add(SubmenuSetting(context, R.string.paths_submenu, MenuTag.CONFIG_PATHS))
sl.add(SubmenuSetting(context, R.string.gamecube_submenu, MenuTag.CONFIG_GAME_CUBE))
sl.add(SubmenuSetting(context, R.string.wii_submenu, MenuTag.CONFIG_WII))
+ sl.add(SubmenuSetting(context, R.string.achievements_submenu, MenuTag.CONFIG_ACHIEVEMENTS))
sl.add(SubmenuSetting(context, R.string.advanced_submenu, MenuTag.CONFIG_ADVANCED))
sl.add(SubmenuSetting(context, R.string.log_submenu, MenuTag.CONFIG_LOG))
sl.add(SubmenuSetting(context, R.string.debug_submenu, MenuTag.DEBUG))
@@ -246,7 +249,7 @@ class SettingsFragmentPresenter(
FloatSetting.MAIN_EMULATION_SPEED,
R.string.speed_limit,
0,
- 0f,
+ if (AchievementModel.isHardcoreModeActive()) 100f else 0f,
200f,
"%",
1f,
@@ -912,6 +915,113 @@ class SettingsFragmentPresenter(
)
}
+ private fun addAchievementSettings(sl: ArrayList<SettingsItem>) {
+ val achievementsEnabledSetting: AbstractBooleanSetting = object : AbstractBooleanSetting {
+ override val boolean: Boolean
+ get() = BooleanSetting.ACHIEVEMENTS_ENABLED.boolean
+
+ override fun setBoolean(settings: Settings, newValue: Boolean) {
+ BooleanSetting.ACHIEVEMENTS_ENABLED.setBoolean(settings, newValue)
+ if (newValue)
+ AchievementModel.init()
+ else
+ AchievementModel.shutdown()
+ loadSettingsList()
+ }
+
+ override val isOverridden: Boolean
+ get() = BooleanSetting.ACHIEVEMENTS_ENABLED.isOverridden
+
+ override val isRuntimeEditable: Boolean
+ get() = BooleanSetting.ACHIEVEMENTS_ENABLED.isRuntimeEditable
+
+ override fun delete(settings: Settings): Boolean {
+ val result = BooleanSetting.ACHIEVEMENTS_ENABLED.delete(settings)
+ AchievementModel.shutdown()
+ loadSettingsList()
+ return result
+ }
+ }
+
+ sl.add(
+ SwitchSetting(
+ context,
+ achievementsEnabledSetting,
+ R.string.achievements_enabled,
+ 0
+ )
+ )
+ if (BooleanSetting.ACHIEVEMENTS_ENABLED.boolean) {
+ if (StringSetting.ACHIEVEMENTS_API_TOKEN.string == "") {
+ sl.add(
+ RunRunnable(
+ context,
+ R.string.achievements_login,
+ 0,
+ 0,
+ 0,
+ false
+ ) {
+ fragmentView.showDialogFragment(LoginDialog(this))
+ loadSettingsList()
+ })
+ } else {
+ sl.add(
+ RunRunnable(
+ context,
+ R.string.achievements_logout,
+ 0,
+ 0,
+ 0,
+ false
+ ) {
+ logout()
+ loadSettingsList()
+ })
+ }
+ sl.add(
+ SwitchSetting(
+ context,
+ BooleanSetting.ACHIEVEMENTS_HARDCORE_ENABLED,
+ R.string.achievements_hardcore_enabled,
+ 0
+ )
+ )
+ sl.add(
+ SwitchSetting(
+ context,
+ BooleanSetting.ACHIEVEMENTS_UNOFFICIAL_ENABLED,
+ R.string.achievements_unofficial_enabled,
+ 0
+ )
+ )
+ sl.add(
+ SwitchSetting(
+ context,
+ BooleanSetting.ACHIEVEMENTS_ENCORE_ENABLED,
+ R.string.achievements_encore_enabled,
+ 0
+ )
+ )
+ sl.add(
+ SwitchSetting(
+ context,
+ BooleanSetting.ACHIEVEMENTS_SPECTATOR_ENABLED,
+ R.string.achievements_spectator_enabled,
+ 0
+ )
+ )
+ sl.add(
+ SwitchSetting(
+ context,
+ BooleanSetting.ACHIEVEMENTS_PROGRESS_ENABLED,
+ R.string.achievements_progress_enabled,
+ 0
+ )
+ )
+ }
+ }
+
private fun addAdvancedSettings(sl: ArrayList<SettingsItem>) {
val SYNC_GPU_NEVER = 0
val SYNC_GPU_ON_IDLE_SKIP = 1
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
index ddcf1d39b5..2907b948b6 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
@@ -32,6 +32,9 @@ class SwitchSettingViewHolder(
binding.textSettingName.text = item.name
binding.textSettingDescription.text = item.description
+ // Make sure we don't trigger any listener set earlier
+ binding.settingSwitch.setOnCheckedChangeListener(null)
+
binding.settingSwitch.isChecked = setting.isChecked
binding.settingSwitch.isEnabled = setting.isEditable
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.kt
index b668e08da8..6bf9c7c147 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.kt
@@ -19,6 +19,7 @@ import org.dolphinemu.dolphinemu.NativeLibrary
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.activities.EmulationActivity
import org.dolphinemu.dolphinemu.databinding.FragmentIngameMenuBinding
+import org.dolphinemu.dolphinemu.features.settings.model.AchievementModel
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting
import org.dolphinemu.dolphinemu.utils.InsetsHelper
@@ -114,11 +115,16 @@ class MenuFragment : Fragment(), View.OnClickListener {
override fun onResume() {
super.onResume()
val savestatesEnabled = BooleanSetting.MAIN_ENABLE_SAVESTATES.boolean
+ val hardcoreEnabled = AchievementModel.isHardcoreModeActive()
val savestateVisibility = if (savestatesEnabled) View.VISIBLE else View.GONE
binding.menuQuicksave.visibility = savestateVisibility
binding.menuQuickload.visibility = savestateVisibility
binding.menuEmulationSaveRoot.visibility = savestateVisibility
binding.menuEmulationLoadRoot.visibility = savestateVisibility
+ // While technically the option is still enabled, AchievementManager
+ // will block the load and send a message to the screen.
+ binding.menuQuickload.paint.isStrikeThruText = hardcoreEnabled
+ binding.menuEmulationLoadRoot.paint.isStrikeThruText = hardcoreEnabled
}
override fun onDestroyView() {
diff --git a/Source/Android/app/src/main/res/layout/dialog_login.xml b/Source/Android/app/src/main/res/layout/dialog_login.xml
new file mode 100644
index 0000000000..320aed0e6a
--- /dev/null
+++ b/Source/Android/app/src/main/res/layout/dialog_login.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/root"
+ android:minWidth="320dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/username"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="@dimen/spacing_large"
+ android:layout_marginVertical="@dimen/spacing_small"
+ android:paddingTop="@dimen/spacing_medlarge"
+ app:layout_constraintBottom_toTopOf="@id/password"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/username_input"
+ android:hint="@string/achievements_username"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:importantForAutofill="no"
+ android:inputType="text"
+ android:minHeight="48dp"
+ android:textAlignment="viewStart" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/password"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="@dimen/spacing_large"
+ app:layout_constraintBottom_toTopOf="@id/login_in_progress"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/username">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/password_input"
+ android:hint="@string/achievements_password"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="48dp"
+ android:importantForAutofill="no"
+ android:inputType="textPassword"
+ android:textAlignment="viewStart" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <TextView
+ android:id="@+id/login_in_progress"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:text="@string/achievements_login_in_progress"
+ android:textAlignment="center"
+ android:textColor="?attr/colorOnErrorContainer"
+ android:visibility="invisible"
+ android:clickable="false"
+ android:focusable="false"
+ app:layout_constraintBottom_toTopOf="@id/login_failed"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/password"/>
+
+ <TextView
+ android:id="@+id/login_failed"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:text="@string/achievements_login_failed"
+ android:textAlignment="center"
+ android:textColor="?attr/colorOnErrorContainer"
+ android:visibility="invisible"
+ android:clickable="false"
+ android:focusable="false"
+ app:layout_constraintBottom_toTopOf="@id/button_cancel"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/login_in_progress"/>
+
+ <Button
+ android:id="@+id/button_cancel"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/spacing_large"
+ android:text="@string/cancel"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/button_login"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/login_failed" />
+
+ <Button
+ android:id="@+id/button_login"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/spacing_large"
+ android:text="@string/ok"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/button_cancel"
+ app:layout_constraintTop_toBottomOf="@id/login_failed" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml
index 6342e35cb0..c94073a213 100644
--- a/Source/Android/app/src/main/res/values/strings.xml
+++ b/Source/Android/app/src/main/res/values/strings.xml
@@ -75,6 +75,7 @@
<string name="enable_cheats">Enable Cheats</string>
<string name="speed_limit">Speed Limit (0% = Unlimited)</string>
<string name="overclock_warning">WARNING: Changing this from the default (100%) WILL break games and cause glitches. Please do not report bugs that occur with a non-default clock.</string>
+ <string name="achievements_submenu">RetroAchievements</string>
<string name="gamecube_submenu">GameCube</string>
<string name="ipl_settings">IPL Settings</string>
<string name="skip_main_menu">Skip Main Menu</string>
@@ -953,4 +954,18 @@ It can efficiently compress both junk data and encrypted Wii data.
<string name="mute_wii_speak">Mute Wii Speak</string>
<string name="wii_speak_permission_warning">Missing Microphone Permission</string>
<string name="wii_speak_permission_warning_description">Wii Speak emulation requires microphone permission. You might need to restart the game for the permission to be effective.</string>
+
+ <!-- Achievements -->
+ <string name="achievements_enabled">Enable Achievements</string>
+ <string name="achievements_hardcore_enabled">Enable Hardcore Mode</string>
+ <string name="achievements_unofficial_enabled">Enable Unofficial Achievements</string>
+ <string name="achievements_encore_enabled">Enable Encore Mode</string>
+ <string name="achievements_spectator_enabled">Enable Spectator Mode</string>
+ <string name="achievements_progress_enabled">Enable Progress Notifications</string>
+ <string name="achievements_username">Username</string>
+ <string name="achievements_password">Password</string>
+ <string name="achievements_login">Log In</string>
+ <string name="achievements_logout">Log Out</string>
+ <string name="achievements_login_in_progress">Logging In</string>
+ <string name="achievements_login_failed">Login Failed</string>
</resources>
diff --git a/Source/Android/jni/AchievementAdapter.cpp b/Source/Android/jni/AchievementAdapter.cpp
new file mode 100644
index 0000000000..d5df74cfdc
--- /dev/null
+++ b/Source/Android/jni/AchievementAdapter.cpp
@@ -0,0 +1,58 @@
+// Copyright 2025 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <jni.h>
+
+#include <latch>
+#include "Common/Event.h"
+#include "Common/HookableEvent.h"
+#include "Core/AchievementManager.h"
+#include "jni/AndroidCommon/AndroidCommon.h"
+#include "jni/AndroidCommon/IDCache.h"
+
+extern "C" {
+
+JNIEXPORT void JNICALL
+Java_org_dolphinemu_dolphinemu_features_settings_model_AchievementModel_init(JNIEnv* env, jclass)
+{
+ AchievementManager::GetInstance().Init(nullptr);
+}
+
+JNIEXPORT jboolean JNICALL
+Java_org_dolphinemu_dolphinemu_features_settings_model_AchievementModel_login(JNIEnv* env, jclass,
+ jstring password)
+{
+ auto& instance = AchievementManager::GetInstance();
+ bool success;
+ std::latch login_complete_event{1};
+ Common::EventHook login_hook =
+ instance.login_event.Register([&login_complete_event, &success](int result) {
+ success = (result == RC_OK);
+ login_complete_event.count_down();
+ });
+ instance.Login(GetJString(env, password));
+ login_complete_event.wait();
+ return success;
+}
+
+JNIEXPORT void JNICALL
+Java_org_dolphinemu_dolphinemu_features_settings_model_AchievementModel_logout(JNIEnv* env, jclass)
+{
+ AchievementManager::GetInstance().Logout();
+}
+
+JNIEXPORT jboolean JNICALL
+Java_org_dolphinemu_dolphinemu_features_settings_model_AchievementModel_isHardcoreModeActive(
+ JNIEnv* env, jclass)
+{
+ return AchievementManager::GetInstance().IsHardcoreModeActive();
+}
+
+JNIEXPORT void JNICALL
+Java_org_dolphinemu_dolphinemu_features_settings_model_AchievementModel_shutdown(JNIEnv* env,
+ jclass)
+{
+ AchievementManager::GetInstance().Shutdown();
+}
+
+} // extern "C"
diff --git a/Source/Android/jni/CMakeLists.txt b/Source/Android/jni/CMakeLists.txt
index c6d542bf08..4f9c015102 100644
--- a/Source/Android/jni/CMakeLists.txt
+++ b/Source/Android/jni/CMakeLists.txt
@@ -1,4 +1,5 @@
add_library(main SHARED
+ AchievementAdapter.cpp
ActivityTracker.cpp
Cheats/ARCheat.cpp
Cheats/Cheats.h
diff --git a/Source/Android/jni/Config/NativeConfig.cpp b/Source/Android/jni/Config/NativeConfig.cpp
index f367c5e162..8478174dae 100644
--- a/Source/Android/jni/Config/NativeConfig.cpp
+++ b/Source/Android/jni/Config/NativeConfig.cpp
@@ -48,6 +48,10 @@ static Config::Location GetLocation(JNIEnv* env, jstring file, jstring section,
{
system = Config::System::GameSettingsOnly;
}
+ else if (decoded_file == "RetroAchievements")
+ {
+ system = Config::System::Achievements;
+ }
else
{
ASSERT(false);
diff --git a/Source/Android/jni/MainAndroid.cpp b/Source/Android/jni/MainAndroid.cpp
index 707e81e336..7c78f49045 100644
--- a/Source/Android/jni/MainAndroid.cpp
+++ b/Source/Android/jni/MainAndroid.cpp
@@ -33,6 +33,7 @@
#include "Common/Version.h"
#include "Common/WindowSystemInfo.h"
+#include "Core/AchievementManager.h"
#include "Core/Boot/Boot.h"
#include "Core/BootManager.h"
#include "Core/CommonTitles.h"
@@ -559,6 +560,8 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_Initialize(J
WiimoteReal::InitAdapterClass();
UICommon::Init();
UICommon::InitControllers(WindowSystemInfo(WindowSystemType::Android, nullptr, nullptr, nullptr));
+
+ AchievementManager::GetInstance().Init(nullptr);
}
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_ReportStartToAnalytics(JNIEnv*,