diff --git a/README.md b/README.md index 5daf7d4e7..c34e08949 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,14 @@ As the name alludes, BCR intends to be a basic as possible. The project will hav * **For OnePlus and Realme devices running the stock firmware (or custom firmware based on the stock firmware)**, also extract the `.apk` from the zip and install it manually before rebooting. This is necessary to work around a bug in the firmware where the app data directory does not get created, causing BCR to open up to a blank screen. * **For unrooted custom firmware**, flash the zip while booted into recovery. + * **NOTE**: The `READ_CALL_LOG` permission is hard restricted in Android 10+, which prevents it from being granted, even via Android's settings. To remove this restriction, run via adb: + ```bash + # If rooted, run inside of `su`: + CLASSPATH=/system/priv-app/com.chiller3.bcr/app-release.apk app_process / com.chiller3.bcr.standalone.RemoveHardRestrictionsKt + + # If unrooted, install BCR as both a user app and a system app: + pm install /system/priv-app/com.chiller3.bcr/app-release.apk + ``` * **NOTE**: If the custom firmware's `system` partition is formatted with `erofs`, then the filesystem is read-only and it is not possible to use this method. * Manually extracting the files from the `system/` folder in the zip will also work as long as the files have `644` permissions and the `u:object_r:system_file:s0` SELinux label. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 86a10294b..30c04a6ee 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -350,6 +350,8 @@ android.applicationVariants.all { } } + from(File(magiskDir, "customize.sh")) + from(File(rootDir, "LICENSE")) from(File(rootDir, "README.md")) } diff --git a/app/magisk/customize.sh b/app/magisk/customize.sh new file mode 100644 index 000000000..725249555 --- /dev/null +++ b/app/magisk/customize.sh @@ -0,0 +1,24 @@ +# READ_CALL_LOG is a hard-restricted permission in Android 10+. It cannot be +# granted by the user unless it is exempted by the system. The most common way +# to do this is via the installer, but that's not applicable when adding new +# system apps. Instead, we talk to the permission service directly over binder +# to alter the flags. This requires flashing a second time after a reboot so +# that the package manager is already aware of BCR. + +app_id=$(grep '^id=' "${MODPATH}/module.prop" | cut -d= -f2) + +CLASSPATH=$(find "${MODPATH}"/system/priv-app/"${app_id}" -name '*.apk') \ + app_process \ + / \ + com.chiller3.bcr.standalone.RemoveHardRestrictionsKt \ + 2>&1 + +case "${?}" in +0|2) + exit 0 + ;; +*) + rm -rv "${MODPATH}" 2>&1 + exit 1 + ;; +esac diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index e396b8a1b..a2da46eaf 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -29,3 +29,8 @@ -keepclassmembers class androidx.documentfile.provider.TreeDocumentFile { (androidx.documentfile.provider.DocumentFile, android.content.Context, android.net.Uri); } + +# Keep standalone CLI utilities +-keep class com.chiller3.bcr.standalone.* { + *; +} \ No newline at end of file diff --git a/app/src/main/java/com/chiller3/bcr/standalone/RemoveHardRestrictions.kt b/app/src/main/java/com/chiller3/bcr/standalone/RemoveHardRestrictions.kt new file mode 100644 index 000000000..3edc262a0 --- /dev/null +++ b/app/src/main/java/com/chiller3/bcr/standalone/RemoveHardRestrictions.kt @@ -0,0 +1,257 @@ +@file:SuppressLint( + "BlockedPrivateApi", + "DiscouragedPrivateApi", + "PrivateApi", + "SoonBlockedPrivateApi", +) + +package com.chiller3.bcr.standalone + +import android.Manifest +import android.annotation.SuppressLint +import android.content.pm.PackageManager +import android.os.Build +import android.os.Process +import android.system.ErrnoException +import androidx.annotation.RequiresApi +import com.chiller3.bcr.BuildConfig +import kotlin.system.exitProcess + +private object ActivityThreadProxy { + private val CLS = Class.forName("android.app.ActivityThread") + private val METHOD_GET_PACKAGE_MANAGER = CLS.getDeclaredMethod("getPackageManager") + private val METHOD_GET_PERMISSION_MANAGER = CLS.getDeclaredMethod("getPermissionManager") + + fun getPackageManager(): PackageManagerProxy { + val iface = METHOD_GET_PACKAGE_MANAGER.invoke(null)!! + return PackageManagerProxy(iface) + } + + @RequiresApi(Build.VERSION_CODES.R) + fun getPermissionManager(): PermissionManagerProxy { + val iface = METHOD_GET_PERMISSION_MANAGER.invoke(null)!! + return PermissionManagerProxy(iface) + } +} + +private class PackageManagerProxy(private val iface: Any) { + companion object { + private val CLS = Class.forName("android.content.pm.IPackageManager") + private val METHOD_IS_PACKAGE_AVAILABLE = CLS.getDeclaredMethod( + "isPackageAvailable", String::class.java, Int::class.java) + // Android 10 only + private val METHOD_GET_PERMISSION_FLAGS by lazy { + CLS.getDeclaredMethod( + "getPermissionFlags", + String::class.java, + String::class.java, + Int::class.java, + ) + } + // Android 10 only + private val METHOD_UPDATE_PERMISSION_FLAGS by lazy { + CLS.getDeclaredMethod( + "updatePermissionFlags", + String::class.java, + String::class.java, + Int::class.java, + Int::class.java, + Int::class.java, + ) + } + + private val WRAPPER_CLS = PackageManager::class.java + val FLAG_PERMISSION_APPLY_RESTRICTION = WRAPPER_CLS.getDeclaredField( + "FLAG_PERMISSION_APPLY_RESTRICTION").getInt(null) + val FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT = WRAPPER_CLS.getDeclaredField( + "FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT").getInt(null) + val FLAG_PERMISSION_RESTRICTION_ANY_EXEMPT = WRAPPER_CLS.getDeclaredField( + "FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT").getInt(null) + } + + fun isPackageAvailable(packageName: String, userId: Int): Boolean { + return METHOD_IS_PACKAGE_AVAILABLE.invoke(iface, packageName, userId) as Boolean + } + + fun getPermissionFlags(permissionName: String, packageName: String, userId: Int): Int { + return METHOD_GET_PERMISSION_FLAGS.invoke(iface, permissionName, packageName, userId) as Int + } + + fun updatePermissionFlags( + permissionName: String, + packageName: String, + flagMask: Int, + flagValues: Int, + userId: Int, + ) { + METHOD_UPDATE_PERMISSION_FLAGS.invoke( + iface, + permissionName, + packageName, + flagMask, + flagValues, + userId, + ) + } + +} + +@RequiresApi(Build.VERSION_CODES.R) +private class PermissionManagerProxy(private val iface: Any) { + companion object { + private val CLS = Class.forName("android.permission.IPermissionManager") + private val METHOD_GET_PERMISSION_FLAGS = + CLS.getDeclaredMethod( + "getPermissionFlags", + String::class.java, + String::class.java, + Int::class.java, + ) + private val METHOD_UPDATE_PERMISSION_FLAGS = + CLS.getDeclaredMethod( + "updatePermissionFlags", + String::class.java, + String::class.java, + Int::class.java, + Int::class.java, + Boolean::class.java, + Int::class.java, + ) + } + + fun getPermissionFlags(packageName: String, permissionName: String, userId: Int): Int { + return METHOD_GET_PERMISSION_FLAGS(iface, packageName, permissionName, userId) as Int + } + + fun updatePermissionFlags( + packageName: String, + permissionName: String, + flagMask: Int, + flagValues: Int, + checkAdjustPolicyFlagPermission: Boolean, + userId: Int, + ) { + METHOD_UPDATE_PERMISSION_FLAGS.invoke( + iface, + packageName, + permissionName, + flagMask, + flagValues, + checkAdjustPolicyFlagPermission, + userId, + ) + } +} + +private fun switchToSystemUid() { + if (Process.myUid() != Process.SYSTEM_UID) { + val setUid = Process::class.java.getDeclaredMethod("setUid", Int::class.java) + val errno = setUid.invoke(null, Process.SYSTEM_UID) as Int + + if (errno != 0) { + throw Exception("Failed to switch to SYSTEM (${Process.SYSTEM_UID}) user", + ErrnoException("setuid", errno)) + } + if (Process.myUid() != Process.SYSTEM_UID) { + throw IllegalStateException("UID didn't actually change: " + + "${Process.myUid()} != ${Process.SYSTEM_UID}") + } + } +} + +@Suppress("SameParameterValue") +private fun removeRestriction(packageName: String, permission: String, userId: Int): Boolean { + val packageManager = ActivityThreadProxy.getPackageManager() + if (!packageManager.isPackageAvailable(packageName, userId)) { + throw IllegalArgumentException("Package $packageName is not installed for user $userId") + } + + val (getFlags, updateFlags) = if (Build.VERSION.SDK_INT in + Build.VERSION_CODES.R..Build.VERSION_CODES.TIRAMISU) { + val permissionManager = ActivityThreadProxy.getPermissionManager() + + Pair( + { permissionManager.getPermissionFlags(packageName, permission, userId) }, + { mask: Int, set: Int -> + permissionManager.updatePermissionFlags( + packageName, permission, mask, set, false, userId) + }, + ) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + Pair( + { packageManager.getPermissionFlags(permission, packageName, userId) }, + { mask: Int, set: Int -> + packageManager.updatePermissionFlags(permission, packageName, mask, set, userId) + }, + ) + } else { + throw IllegalStateException("Not supported on SDK version ${Build.VERSION.SDK_INT}") + } + + val oldFlags = getFlags() + + updateFlags( + PackageManagerProxy.FLAG_PERMISSION_RESTRICTION_ANY_EXEMPT or + PackageManagerProxy.FLAG_PERMISSION_APPLY_RESTRICTION, + PackageManagerProxy.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT, + ) + + val newFlags = getFlags() + if (newFlags and PackageManagerProxy.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT == 0) { + throw IllegalStateException("RESTRICTION_SYSTEM_EXEMPT flag did not get added") + } + if (newFlags and PackageManagerProxy.FLAG_PERMISSION_APPLY_RESTRICTION != 0) { + throw IllegalStateException("APPLY_RESTRICTION flag did not get removed") + } + + return newFlags != oldFlags +} + +fun mainInternal() { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) { + // Android 9 does not have FLAG_PERMISSION_APPLY_RESTRICTION + System.err.println("Android 9 does not have hard-restricted permissions") + return + } + + switchToSystemUid() + + val packageManager = ActivityThreadProxy.getPackageManager() + if (!packageManager.isPackageAvailable(BuildConfig.APPLICATION_ID, 0)) { + System.err.println(""" + ---------------- NOTE ---------------- + Android 10+ marks the READ_CALL_LOG + permission as being hard restricted. + This makes it impossible to grant the + (optional) permission, even from + Android's settings. To remove this + restriction for BCR only, reboot and + reflash one more time. This procedure + only needs to be done once and will + persist across upgrades. This does not + affect other apps and the changes go + away when BCR is uninstalled. + -------------------------------------- + """.trimIndent()) + exitProcess(2) + } + + val changed = removeRestriction(BuildConfig.APPLICATION_ID, Manifest.permission.READ_CALL_LOG, 0) + val suffix = "from ${BuildConfig.APPLICATION_ID} for ${Manifest.permission.READ_CALL_LOG}" + + if (changed) { + println("Successfully removed hard restriction $suffix") + } else { + println("Hard restriction already removed $suffix") + } +} + +fun main() { + try { + mainInternal() + } catch (e: Exception) { + // Otherwise, exceptions go to the logcat + e.printStackTrace() + exitProcess(1) + } +} \ No newline at end of file