Skip to content

Commit 48a7c96

Browse files
committed
fix: request users to grant 'PROJECT_MEDIA' permission on Android 15+ due to new screen sharing restrictions
1 parent bf6af68 commit 48a7c96

File tree

6 files changed

+88
-51
lines changed

6 files changed

+88
-51
lines changed

app/src/main/java/me/timschneeberger/rootlessjamesdsp/activity/MainActivity.kt

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import me.timschneeberger.rootlessjamesdsp.utils.extensions.ContextExtensions.sh
7070
import me.timschneeberger.rootlessjamesdsp.utils.extensions.ContextExtensions.toast
7171
import me.timschneeberger.rootlessjamesdsp.utils.extensions.ContextExtensions.unregisterLocalReceiver
7272
import me.timschneeberger.rootlessjamesdsp.utils.extensions.PermissionExtensions.hasDumpPermission
73+
import me.timschneeberger.rootlessjamesdsp.utils.extensions.PermissionExtensions.hasProjectMediaAppOp
7374
import me.timschneeberger.rootlessjamesdsp.utils.extensions.PermissionExtensions.hasRecordPermission
7475
import me.timschneeberger.rootlessjamesdsp.utils.isPlugin
7576
import me.timschneeberger.rootlessjamesdsp.utils.isRoot
@@ -382,9 +383,7 @@ class MainActivity : BaseActivity() {
382383
}
383384
}
384385

385-
if (isRootless() && SdkCheck.isVanillaIceCream) {
386-
showAndroid15Alert()
387-
}
386+
showAndroid15Alert()
388387

389388
dspFragment.setUpdateCardOnClick { updateManager.installUpdate(this) }
390389
dspFragment.setUpdateCardOnCloseClick(::dismissUpdate)
@@ -454,6 +453,24 @@ class MainActivity : BaseActivity() {
454453
}
455454

456455
private fun showAndroid15Alert() {
456+
if(!isRootless() || !SdkCheck.isVanillaIceCream)
457+
return
458+
459+
if(!hasProjectMediaAppOp()) {
460+
MaterialAlertDialogBuilder(this)
461+
.setTitle(R.string.android_15_screenshare_warning_title)
462+
.setMessage(R.string.android_15_screenshare_keyguard_warning)
463+
.setCancelable(false)
464+
.setPositiveButton(R.string.continue_action) { _, _ ->
465+
startActivity(Intent(this, OnboardingActivity::class.java).apply {
466+
putExtra(OnboardingActivity.EXTRA_FIX_PERMS, false)
467+
})
468+
this.finish()
469+
}
470+
.show()
471+
return
472+
}
473+
457474
if(prefsVar.get<Boolean>(R.string.key_android15_screenrecord_restriction_seen))
458475
return
459476

app/src/main/java/me/timschneeberger/rootlessjamesdsp/fragment/OnboardingFragment.kt

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import me.timschneeberger.rootlessjamesdsp.utils.extensions.ContextExtensions.sh
3838
import me.timschneeberger.rootlessjamesdsp.utils.extensions.ContextExtensions.toast
3939
import me.timschneeberger.rootlessjamesdsp.utils.extensions.PermissionExtensions.hasDumpPermission
4040
import me.timschneeberger.rootlessjamesdsp.utils.extensions.PermissionExtensions.hasNotificationPermission
41+
import me.timschneeberger.rootlessjamesdsp.utils.extensions.PermissionExtensions.hasProjectMediaAppOp
4142
import me.timschneeberger.rootlessjamesdsp.utils.extensions.PermissionExtensions.hasRecordPermission
4243
import me.timschneeberger.rootlessjamesdsp.utils.isRootless
4344
import me.timschneeberger.rootlessjamesdsp.utils.preferences.Preferences
@@ -443,12 +444,18 @@ class OnboardingFragment : Fragment() {
443444
goToPage(nextIndex)
444445
}
445446

447+
private fun areAdbPermissionsGranted(): Boolean {
448+
return requireContext().hasDumpPermission() &&
449+
// Android 15+ doesn't allow keyguard recording without PROJECT_MEDIA
450+
requireContext().hasProjectMediaAppOp()
451+
}
452+
446453
private fun requestNextPage(nextPage: Int, forward: Boolean): Int
447454
{
448455
val shouldSkip = when (nextPage) {
449456
// Don't skip ADB setup if redoAdbSetup is set
450-
PAGE_METHOD_SELECT -> requireContext().hasDumpPermission() && !redoAdbSetup
451-
PAGE_ADB_SETUP -> requireContext().hasDumpPermission() && !redoAdbSetup
457+
PAGE_METHOD_SELECT -> areAdbPermissionsGranted() && !redoAdbSetup
458+
PAGE_ADB_SETUP -> areAdbPermissionsGranted() && !redoAdbSetup
452459
PAGE_RUNTIME_PERMISSIONS -> {
453460
requireContext().hasNotificationPermission() && requireContext().hasRecordPermission()
454461
}
@@ -466,19 +473,19 @@ class OnboardingFragment : Fragment() {
466473
private fun canAccessNextPage(currentPage: Int): Boolean
467474
{
468475
return when (currentPage) {
469-
PAGE_ADB_SETUP -> ensureDumpPermission()
476+
PAGE_ADB_SETUP -> ensureAdbPermissions()
470477
PAGE_RUNTIME_PERMISSIONS -> ensureRuntimePermissions()
471478
else -> true
472479
}
473480
}
474481

475-
private fun ensureDumpPermission(): Boolean{
482+
private fun ensureAdbPermissions(): Boolean {
476483
/* Permission already granted?
477484
* Note: If were are redoing the ADB setup, make sure that the Shizuku setup
478485
* can run regardless to grant optional permissions
479486
*/
480-
if(requireContext().hasDumpPermission() && (!redoAdbSetup || selectedSetupMethod != SetupMethods.Shizuku)) {
481-
Timber.d("DUMP permission granted")
487+
if(areAdbPermissionsGranted() && (!redoAdbSetup || selectedSetupMethod != SetupMethods.Shizuku)) {
488+
Timber.d("ADB permissions already granted")
482489
return true
483490
}
484491

@@ -510,55 +517,56 @@ class OnboardingFragment : Fragment() {
510517

511518
// Grant permanent SYSTEM_ALERT_WINDOW op as shell
512519
try {
513-
val result = ShizukuSystemServerApi.AppOpsService_setMode(
520+
ShizukuSystemServerApi.AppOpsService_setMode(
514521
ShizukuSystemServerApi.APP_OPS_OP_SYSTEM_ALERT_WINDOW,
515522
uid,
516523
pkg,
517524
ShizukuSystemServerApi.APP_OPS_MODE_ALLOW
518525
)
519-
if(!result)
520-
Timber.e("AppOpsService_setMode for system_alert_window failed")
521526
}
522527
catch (ex: Exception) {
523528
Timber.e("AppOpsService_setMode for system_alert_window threw an exception")
524529
Timber.e(ex)
530+
ShizukuSystemServerApi.exec("appops set $pkg SYSTEM_ALERT_WINDOW allow")
525531
}
526532

527533
// Grant permanent PROJECT_MEDIA op as shell
528534
try {
529-
val result = ShizukuSystemServerApi.AppOpsService_setMode(
535+
ShizukuSystemServerApi.AppOpsService_setMode(
530536
ShizukuSystemServerApi.APP_OPS_OP_PROJECT_MEDIA,
531537
uid,
532538
pkg,
533539
ShizukuSystemServerApi.APP_OPS_MODE_ALLOW
534540
)
535-
if(!result)
536-
Timber.e("AppOpsService_setMode failed")
537541
}
538542
catch (ex: Exception) {
539543
Timber.e("AppOpsService_setMode threw an exception")
540544
Timber.e(ex)
545+
ShizukuSystemServerApi.exec("appops set $pkg PROJECT_MEDIA allow")
541546
}
542547

543548
// Re-check permission
544-
return if (requireContext().hasDumpPermission()) {
545-
Timber.d("DUMP permission via Shizuku granted")
549+
return if (areAdbPermissionsGranted()) {
550+
Timber.d("ADB permissions via Shizuku granted")
546551
true
547552
} else {
548-
Timber.e("DUMP not granted")
553+
Timber.e("ADB permissions not granted")
549554
requireContext().showAlert(R.string.onboarding_adb_shizuku_no_dump_perm_title,
550555
R.string.onboarding_adb_shizuku_no_dump_perm)
551-
552-
// Fallback just in case
553-
@Suppress("DEPRECATION")
554-
val proc = Shizuku.newProcess(arrayOf<String>("pm", "grant", pkg, Manifest.permission.DUMP), null, null)
555-
proc.waitFor()
556556
false
557557
}
558558
}
559559

560-
requireContext().showAlert(R.string.onboarding_adb_dump_not_granted_title,
561-
R.string.onboarding_adb_dump_not_granted)
560+
// Regular ADB setup
561+
if(!requireContext().hasDumpPermission()) {
562+
requireContext().showAlert(R.string.onboarding_adb_not_granted_title,
563+
R.string.onboarding_adb_dump_permission_not_granted)
564+
}
565+
else if(!requireContext().hasProjectMediaAppOp()) {
566+
requireContext().showAlert(R.string.onboarding_adb_not_granted_title,
567+
R.string.onboarding_adb_project_media_not_granted)
568+
}
569+
562570
return false
563571
}
564572

@@ -587,8 +595,8 @@ class OnboardingFragment : Fragment() {
587595
val page = binding.adbSetup
588596

589597
page.step4.isVisible = selectedSetupMethod == SetupMethods.Adb
590-
page.step5.isVisible = selectedSetupMethod == SetupMethods.Adb
591-
page.step5bOptional.isVisible = selectedSetupMethod == SetupMethods.Adb && isRootless()
598+
page.step6.isVisible = selectedSetupMethod == SetupMethods.Adb
599+
page.step5b.isVisible = selectedSetupMethod == SetupMethods.Adb && isRootless()
592600
page.step5cOptional.isVisible = selectedSetupMethod == SetupMethods.Adb && isRootless()
593601

594602
if(selectedSetupMethod == SetupMethods.Shizuku) {
@@ -632,8 +640,8 @@ class OnboardingFragment : Fragment() {
632640
page.step2.bodyText = getString(R.string.onboarding_adb_manual_step2)
633641
page.step3.bodyText = getString(R.string.onboarding_adb_manual_step3)
634642
page.step4.bodyText = getString(R.string.onboarding_adb_manual_step4, requireContext().packageName)
635-
page.step5.bodyText = getString(R.string.onboarding_adb_manual_step5)
636-
page.step5bOptional.bodyText = getString(R.string.onboarding_adb_manual_step5b, requireContext().packageName)
643+
page.step6.bodyText = getString(R.string.onboarding_adb_manual_step5)
644+
page.step5b.bodyText = getString(R.string.onboarding_adb_manual_step5b_required, requireContext().packageName)
637645
page.step5cOptional.bodyText = getString(R.string.onboarding_adb_manual_step5c, requireContext().packageName)
638646

639647
page.title.text = getString(R.string.onboarding_adb_adb_title)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<!-- drawable/numeric_6_circle_outline.xml -->
2+
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
3+
<path android:fillAlpha="0.3"
4+
android:fillColor="@android:color/white"
5+
android:pathData="M12,12m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0" android:strokeAlpha="0.3"/>
6+
<path android:fillColor="@android:color/white"
7+
android:pathData="M11,7H15V9H11V11H13A2,2 0 0,1 15,13V15A2,2 0 0,1 13,17H11A2,2 0 0,1 9,15V9A2,2 0 0,1 11,7M11,13V15H13V13H11M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z" />
8+
</vector>

app/src/main/res/layout/onboarding_page4.xml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,12 @@
8888
app:iconTint="?attr/colorOnSurface" />
8989

9090
<me.timschneeberger.rootlessjamesdsp.view.Card
91-
android:id="@+id/step5b_optional"
91+
android:id="@+id/step5b"
9292
android:layout_width="match_parent"
9393
android:layout_height="wrap_content"
9494
android:visibility="gone"
9595
app:cardMargin="8dp"
96-
app:iconSrc="@drawable/ic_twotone_info_24dp"
96+
app:iconSrc="@drawable/ic_numeric_5_circle_outline"
9797
app:iconTint="?attr/colorOnSurface" />
9898

9999

@@ -107,12 +107,12 @@
107107
app:iconTint="?attr/colorOnSurface" />
108108

109109
<me.timschneeberger.rootlessjamesdsp.view.Card
110-
android:id="@+id/step5"
110+
android:id="@+id/step6"
111111
android:layout_width="match_parent"
112112
android:layout_height="wrap_content"
113113
android:visibility="gone"
114114
app:cardMargin="8dp"
115-
app:iconSrc="@drawable/ic_numeric_5_circle_outline"
115+
app:iconSrc="@drawable/ic_numeric_6_circle_outline"
116116
app:iconTint="?attr/colorOnSurface" />
117117
</LinearLayout>
118118

app/src/main/res/values/strings.xml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
<string name="success">Success</string>
5555
<string name="open">Open</string>
5656
<string name="close">Close</string>
57+
<string name="continue_action">Continue</string>
5758
<string name="tutorial">Tutorial</string>
5859
<string name="never_show_warning_again">Never show this warning again</string>
5960
<string name="delete">Delete</string>
@@ -236,14 +237,15 @@
236237
<string name="onboarding_adb_shizuku_grant_fail_denied">Please grant this application access to Shizuku. You may need to open Shizuku\'s permission manager and grant the permission manually if you previously denied it and the popup no longer shows.</string>
237238
<string name="onboarding_adb_shizuku_no_dump_perm_title">Shizuku failed to grant ADB permissions</string>
238239
<string name="onboarding_adb_shizuku_no_dump_perm">Please wait a few seconds and try again. If it still does not work, try the alternative method.</string>
239-
<string name="onboarding_adb_dump_not_granted_title">Permission not yet granted</string>
240-
<string name="onboarding_adb_dump_not_granted">Please follow the instructions first and try again. The ADB access is not yet set-up properly.</string>
240+
<string name="onboarding_adb_not_granted_title">Permission not yet granted</string>
241+
<string name="onboarding_adb_dump_permission_not_granted">Please follow the instructions first and try again. The DUMP permission has not been granted.</string>
242+
<string name="onboarding_adb_project_media_not_granted">Please follow the instructions first and try again. The PROJECT_MEDIA permission has not been granted.</string>
241243
<string name="onboarding_adb_manual_step1">Go to Android\'s hidden development settings and enable USB debugging. You need to unlock these settings by tapping the build number in the system settings several times.</string>
242244
<string name="onboarding_adb_manual_step2">Open https://app.webadb.com in your computer\'s browser (works best in Chrome) and connect your device using USB to the computer.</string>
243245
<string name="onboarding_adb_manual_step3">Add and connect the USB device in WebADB.</string>
244246
<string name="onboarding_adb_manual_step4">Navigate to \'Interactive Shell\' and execute the following command in the shell: \'pm grant %s android.permission.DUMP\'</string>
245247
<string name="onboarding_adb_manual_step5">Tap \'Next\' in this app to continue.</string>
246-
<string name="onboarding_adb_manual_step5b">(Optional) You can permanently skip the audio capture permission prompts by executing this command: \'appops set %s PROJECT_MEDIA allow\'</string>
248+
<string name="onboarding_adb_manual_step5b_required">Execute the following command to grant permanent permission to project audio: \'appops set %s PROJECT_MEDIA allow\'</string>
247249
<string name="onboarding_adb_manual_step5c">(Optional) In addition to the previous step, you can also enable auto-start by executing the following command: \'appops set %s SYSTEM_ALERT_WINDOW allow\'</string>
248250
<string name="onboarding_adb_manual_step1_button">Open development settings</string>
249251

@@ -659,5 +661,7 @@ Even though this app only records system audio, it is still affected by this res
659661

660662
To workaround this issue, you can enable \'Disable screen share protections\' in the system developer settings.
661663
Tap on \'Tutorial\' below for detailed instructions.</string>
664+
<string name="android_15_screenshare_keyguard_warning">Android 15 introduces a restriction that stops screen sharing when the device is locked. To workaround this issue, the \'PROJECT_MEDIA\' permission must be granted using Shizuku or ADB.
662665

666+
Please press \'Continue\' below to re-do the setup wizard.</string>
663667
</resources>

hidden-api-impl/src/main/java/me/timschneeberger/hiddenapi_impl/ShizukuSystemServerApi.java

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ else if (Build.VERSION.SDK_INT == 34) {
6868
}
6969
}
7070

71-
private static synchronized void exec(String cmd) {
71+
public static synchronized void exec(String cmd) {
7272
try {
7373
Method newProcess = Shizuku.class.getDeclaredMethod("newProcess", String[].class, String[].class, String.class);
7474
newProcess.setAccessible(true);
@@ -94,7 +94,7 @@ private static synchronized void exec(String cmd) {
9494
public static final String APP_OPS_OP_PROJECT_MEDIA = "PROJECT_MEDIA";
9595
public static final String APP_OPS_OP_SYSTEM_ALERT_WINDOW = "SYSTEM_ALERT_WINDOW";
9696

97-
public static boolean AppOpsService_setMode(String op, int packageUid, String packageName, String mode) throws RemoteException {
97+
public static void AppOpsService_setMode(String op, int packageUid, String packageName, String mode) throws RemoteException {
9898
int index = -1;
9999

100100
try {
@@ -117,18 +117,20 @@ public static boolean AppOpsService_setMode(String op, int packageUid, String pa
117117
.getMethod("strOpToOp", String.class);
118118

119119
opIndex = (int) method.invoke(null, op);
120-
}
121-
catch(Exception ignored) {}
122-
try {
123-
Method method = Class.forName("android.app.AppOpsManager")
124-
.getMethod("strDebugOpToOp", String.class);
120+
} catch (Exception e) {
121+
Log.e("ShizukuSystemServerApi", "Failed to get op index via strOpToOp");
125122

126-
opIndex = (int) method.invoke(null, op);
127-
}
128-
catch(Exception ignored) {}
123+
try {
124+
Method methodDbg = Class.forName("android.app.AppOpsManager")
125+
.getMethod("strDebugOpToOp", String.class);
129126

130-
if(index < 0 || opIndex < 0)
131-
return false;
127+
opIndex = (int) methodDbg.invoke(null, op);
128+
}
129+
catch (Exception ex) {
130+
Log.e("ShizukuSystemServerApi", "Failed to get op index via strDebugOpToOp");
131+
throw new RuntimeException(e);
132+
}
133+
}
132134

133135
try {
134136
APP_OPS_SERVICE.getOrThrow().setMode(
@@ -140,10 +142,8 @@ public static boolean AppOpsService_setMode(String op, int packageUid, String pa
140142
}
141143
catch(NullPointerException ex) {
142144
Log.e("ShizukuSystemServerApi", "Failed to call app ops service");
143-
return false;
145+
throw new RuntimeException(ex);
144146
}
145-
146-
return true;
147147
}
148148

149149
public static void AudioPolicyService_setAllowedCapturePolicy(int uid, CapturePolicy capturePolicy) {

0 commit comments

Comments
 (0)