diff --git a/extensions/samsung/radio/build.gradle.kts b/extensions/samsung/radio/build.gradle.kts
new file mode 100644
index 000000000..0eadeef26
--- /dev/null
+++ b/extensions/samsung/radio/build.gradle.kts
@@ -0,0 +1,4 @@
+dependencies {
+ compileOnly(project(":extensions:shared:library"))
+ compileOnly(project(":extensions:samsung:radio:stub"))
+}
diff --git a/extensions/samsung/radio/src/main/AndroidManifest.xml b/extensions/samsung/radio/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..9b65eb06c
--- /dev/null
+++ b/extensions/samsung/radio/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/extensions/samsung/radio/src/main/java/app/revanced/extension/samsung/radio/misc/fix/crash/FixCrashPatch.java b/extensions/samsung/radio/src/main/java/app/revanced/extension/samsung/radio/misc/fix/crash/FixCrashPatch.java
new file mode 100644
index 000000000..72c5addc4
--- /dev/null
+++ b/extensions/samsung/radio/src/main/java/app/revanced/extension/samsung/radio/misc/fix/crash/FixCrashPatch.java
@@ -0,0 +1,24 @@
+package app.revanced.extension.samsung.radio.misc.fix.crash;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@SuppressWarnings("unused")
+public final class FixCrashPatch {
+ /**
+ * Injection point.
+ *
+ * Add the required permissions to the request list to avoid crashes on API 34+.
+ **/
+ public static final String[] fixPermissionRequestList(String[] perms) {
+ List permsList = new ArrayList<>(Arrays.asList(perms));
+ if (permsList.contains("android.permission.POST_NOTIFICATIONS")) {
+ permsList.addAll(Arrays.asList("android.permission.RECORD_AUDIO", "android.permission.READ_PHONE_STATE", "android.permission.FOREGROUND_SERVICE_MICROPHONE"));
+ }
+ if (permsList.contains("android.permission.RECORD_AUDIO")) {
+ permsList.add("android.permission.FOREGROUND_SERVICE_MICROPHONE");
+ }
+ return permsList.toArray(new String[0]);
+ }
+}
diff --git a/extensions/samsung/radio/src/main/java/app/revanced/extension/samsung/radio/restrictions/device/BypassDeviceChecksPatch.java b/extensions/samsung/radio/src/main/java/app/revanced/extension/samsung/radio/restrictions/device/BypassDeviceChecksPatch.java
new file mode 100644
index 000000000..19b6c3e82
--- /dev/null
+++ b/extensions/samsung/radio/src/main/java/app/revanced/extension/samsung/radio/restrictions/device/BypassDeviceChecksPatch.java
@@ -0,0 +1,19 @@
+package app.revanced.extension.samsung.radio.restrictions.device;
+
+import android.os.SemSystemProperties;
+
+import java.util.Arrays;
+
+@SuppressWarnings("unused")
+public final class BypassDeviceChecksPatch {
+
+ /**
+ * Injection point.
+ *
+ * Check if the device has the required hardware
+ **/
+ public static final boolean checkIfDeviceIsIncompatible(String[] deviceList) {
+ String currentDevice = SemSystemProperties.getSalesCode();
+ return Arrays.asList(deviceList).contains(currentDevice);
+ }
+}
diff --git a/extensions/samsung/radio/stub/build.gradle.kts b/extensions/samsung/radio/stub/build.gradle.kts
new file mode 100644
index 000000000..b4bee8809
--- /dev/null
+++ b/extensions/samsung/radio/stub/build.gradle.kts
@@ -0,0 +1,17 @@
+plugins {
+ alias(libs.plugins.android.library)
+}
+
+android {
+ namespace = "app.revanced.extension"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 24
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+}
diff --git a/extensions/samsung/radio/stub/src/main/AndroidManifest.xml b/extensions/samsung/radio/stub/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..15e7c2ae6
--- /dev/null
+++ b/extensions/samsung/radio/stub/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/extensions/samsung/radio/stub/src/main/java/android/os/SemSystemProperties.java b/extensions/samsung/radio/stub/src/main/java/android/os/SemSystemProperties.java
new file mode 100644
index 000000000..33a4b4400
--- /dev/null
+++ b/extensions/samsung/radio/stub/src/main/java/android/os/SemSystemProperties.java
@@ -0,0 +1,7 @@
+package android.os;
+
+public class SemSystemProperties {
+ public static String getSalesCode() {
+ throw new UnsupportedOperationException("Stub");
+ }
+}
\ No newline at end of file
diff --git a/patches/api/patches.api b/patches/api/patches.api
index b15bb5bd3..20f74169a 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -764,6 +764,14 @@ public final class app/revanced/patches/reddit/misc/tracking/url/SanitizeUrlQuer
public static final fun getSanitizeUrlQueryPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/samsung/radio/misc/fix/crash/FixCrashPatchKt {
+ public static final fun getFixCrashPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
+public final class app/revanced/patches/samsung/radio/restrictions/device/BypassDeviceChecksPatchKt {
+ public static final fun getBypassDeviceChecksPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/serviceportalbund/detection/root/RootDetectionPatchKt {
public static final fun getRootDetectionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/samsung/radio/misc/fix/crash/AddManifestPermissionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/samsung/radio/misc/fix/crash/AddManifestPermissionsPatch.kt
new file mode 100644
index 000000000..35641f2e7
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/samsung/radio/misc/fix/crash/AddManifestPermissionsPatch.kt
@@ -0,0 +1,34 @@
+package app.revanced.patches.samsung.radio.misc.fix.crash
+
+import app.revanced.patcher.patch.resourcePatch
+import app.revanced.util.asSequence
+import org.w3c.dom.Element
+
+@Suppress("unused")
+internal val addManifestPermissionsPatch = resourcePatch {
+
+ val requiredPermissions = listOf(
+ "android.permission.READ_PHONE_STATE",
+ "android.permission.FOREGROUND_SERVICE_MICROPHONE",
+ "android.permission.RECORD_AUDIO",
+ )
+
+ execute {
+ document("AndroidManifest.xml").use { document ->
+ document.getElementsByTagName("manifest").item(0).let { manifestEl ->
+
+ // Check which permissions are missing
+ val existingPermissionNames = document.getElementsByTagName("uses-permission").asSequence()
+ .mapNotNull { (it as? Element)?.getAttribute("android:name") }.toSet()
+ val missingPermissions = requiredPermissions.filterNot { it in existingPermissionNames }
+
+ // Then add them
+ for (permission in missingPermissions) {
+ val element = document.createElement("uses-permission")
+ element.setAttribute("android:name", permission)
+ manifestEl.appendChild(element)
+ }
+ }
+ }
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/samsung/radio/misc/fix/crash/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/samsung/radio/misc/fix/crash/Fingerprints.kt
new file mode 100644
index 000000000..f842a45cc
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/samsung/radio/misc/fix/crash/Fingerprints.kt
@@ -0,0 +1,18 @@
+@file:Suppress("unused")
+
+package app.revanced.patches.samsung.radio.misc.fix.crash
+
+import app.revanced.patcher.fingerprint
+import app.revanced.patches.all.misc.transformation.IMethodCall
+import app.revanced.patches.all.misc.transformation.fromMethodReference
+import app.revanced.util.getReference
+import com.android.tools.smali.dexlib2.iface.reference.MethodReference
+
+internal val permissionRequestListFingerprint = fingerprint {
+ strings(
+ "android.permission.POST_NOTIFICATIONS",
+ "android.permission.READ_MEDIA_AUDIO",
+ "android.permission.RECORD_AUDIO"
+ )
+ custom { method, _ -> method.name == "" }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/samsung/radio/misc/fix/crash/FixCrashPatch.kt b/patches/src/main/kotlin/app/revanced/patches/samsung/radio/misc/fix/crash/FixCrashPatch.kt
new file mode 100644
index 000000000..a076ca830
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/samsung/radio/misc/fix/crash/FixCrashPatch.kt
@@ -0,0 +1,42 @@
+@file:Suppress("unused")
+
+package app.revanced.patches.samsung.radio.misc.fix.crash
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
+import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patches.samsung.radio.restrictions.device.bypassDeviceChecksPatch
+import app.revanced.util.findInstructionIndicesReversedOrThrow
+import app.revanced.util.indexOfFirstInstruction
+import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
+
+private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/samsung/radio/misc/fix/crash/FixCrashPatch;"
+
+val fixCrashPatch = bytecodePatch(
+ name = "Fix crashes", description = "Prevents the app from crashing because of missing system permissions."
+) {
+ dependsOn(addManifestPermissionsPatch, bypassDeviceChecksPatch)
+ extendWith("extensions/samsung/radio.rve")
+ compatibleWith("com.sec.android.app.fm"("12.4.00.7", "12.3.00.13", "12.3.00.11"))
+
+ execute {
+ permissionRequestListFingerprint.method.apply {
+ findInstructionIndicesReversedOrThrow(Opcode.FILLED_NEW_ARRAY).forEach { filledNewArrayIndex ->
+ val moveResultIndex = indexOfFirstInstruction(filledNewArrayIndex, Opcode.MOVE_RESULT_OBJECT)
+ if (moveResultIndex < 0) return@forEach // No move-result-object found after the filled-new-array
+
+ // Get the register where the array is saved
+ val arrayRegister = getInstruction(moveResultIndex).registerA
+
+ // Invoke the method from the extension
+ addInstructions(
+ moveResultIndex + 1, """
+ invoke-static { v$arrayRegister }, ${EXTENSION_CLASS_DESCRIPTOR}->fixPermissionRequestList([Ljava/lang/String;)[Ljava/lang/String;
+ move-result-object v$arrayRegister
+ """
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/revanced/patches/samsung/radio/restrictions/device/BypassDeviceChecksPatch.kt b/patches/src/main/kotlin/app/revanced/patches/samsung/radio/restrictions/device/BypassDeviceChecksPatch.kt
new file mode 100644
index 000000000..68ef9a801
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/samsung/radio/restrictions/device/BypassDeviceChecksPatch.kt
@@ -0,0 +1,55 @@
+package app.revanced.patches.samsung.radio.restrictions.device
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
+import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.util.findFreeRegister
+import app.revanced.util.getReference
+import app.revanced.util.indexOfFirstInstructionOrThrow
+import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.iface.reference.StringReference
+
+private const val EXTENSION_CLASS_DESCRIPTOR =
+ "Lapp/revanced/extension/samsung/radio/restrictions/device/BypassDeviceChecksPatch;"
+
+@Suppress("unused")
+val bypassDeviceChecksPatch = bytecodePatch(
+ name = "Bypass device checks",
+ description = "Removes firmware and region blacklisting. " +
+ "This patch will still not allow the app to run on devices that do not have the required hardware.",
+) {
+ extendWith("extensions/samsung/radio.rve")
+ compatibleWith("com.sec.android.app.fm"("12.4.00.7", "12.3.00.13", "12.3.00.11"))
+
+ execute {
+ // Return false = The device is not blacklisted
+ checkDeviceFingerprint.method.apply {
+ // Find the first string that start with "SM-", that's the list of incompatible devices
+ val firstStringIndex = indexOfFirstInstructionOrThrow {
+ opcode == Opcode.CONST_STRING &&
+ getReference()?.string?.startsWith("SM-") == true
+ }
+
+ // Find the following filled-new-array (or filled-new-array/range) instruction
+ val filledNewArrayIndex = indexOfFirstInstructionOrThrow(firstStringIndex + 1) {
+ opcode == Opcode.FILLED_NEW_ARRAY || opcode == Opcode.FILLED_NEW_ARRAY_RANGE
+ }
+
+ // Find an available register for our use
+ val resultRegister = findFreeRegister(filledNewArrayIndex + 1)
+
+ // Store the array there and invoke the method that we added to the class earlier
+ addInstructions(
+ filledNewArrayIndex + 1, """
+ move-result-object v$resultRegister
+ invoke-static { v$resultRegister }, $EXTENSION_CLASS_DESCRIPTOR->checkIfDeviceIsIncompatible([Ljava/lang/String;)Z
+ move-result v$resultRegister
+ return v$resultRegister
+ """
+ )
+
+ // Remove the instructions before our strings
+ removeInstructions(0, firstStringIndex)
+ }
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/samsung/radio/restrictions/device/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/samsung/radio/restrictions/device/Fingerprints.kt
new file mode 100644
index 000000000..476823591
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/samsung/radio/restrictions/device/Fingerprints.kt
@@ -0,0 +1,61 @@
+package app.revanced.patches.samsung.radio.restrictions.device
+
+import app.revanced.patcher.fingerprint
+import app.revanced.patches.all.misc.transformation.IMethodCall
+import app.revanced.patches.all.misc.transformation.fromMethodReference
+import app.revanced.util.getReference
+import com.android.tools.smali.dexlib2.iface.reference.MethodReference
+
+internal val checkDeviceFingerprint = fingerprint {
+ returns("Z")
+ custom { method, _ ->
+ /* Check for methods call to:
+ - Landroid/os/SemSystemProperties;->getSalesCode()Ljava/lang/String;
+ - Landroid/os/SemSystemProperties;->getCountryIso()Ljava/lang/String;
+ */
+
+ val impl = method.implementation ?: return@custom false
+
+ // Track which target methods we've found
+ val foundMethods = mutableSetOf()
+
+ // Scan method instructions for calls to our target methods
+ for (instr in impl.instructions) {
+ val ref = instr.getReference() ?: continue
+ val mc = fromMethodReference(ref) ?: continue
+
+ if (mc == MethodCall.GetSalesCode || mc == MethodCall.GetCountryIso) {
+ foundMethods.add(mc)
+
+ // If we found both methods, return success
+ if (foundMethods.size == 2) {
+ return@custom true
+ }
+ }
+ }
+
+ // Only match if both methods are present
+ return@custom false
+ }
+}
+
+// Information about method calls we want to replace
+private enum class MethodCall(
+ override val definedClassName: String,
+ override val methodName: String,
+ override val methodParams: Array,
+ override val returnType: String,
+) : IMethodCall {
+ GetSalesCode(
+ "Landroid/os/SemSystemProperties;",
+ "getSalesCode",
+ arrayOf(),
+ "Ljava/lang/String;",
+ ),
+ GetCountryIso(
+ "Landroid/os/SemSystemProperties;",
+ "getCountryIso",
+ arrayOf(),
+ "Ljava/lang/String;",
+ ),
+}
\ No newline at end of file