Skip to content

Commit 02cd4ce

Browse files
SERDUNdigiboridev
andauthored
feat(android): background-activity-start permission for lock-screen calls (WT-1349) (#337)
* feat(android): add background-activity-start permission for lock-screen calls On MIUI/HyperOS the incoming-call Activity cannot cover the lock screen unless the OEM 'display pop-up windows while running in background' capability (OP_BACKGROUND_START_ACTIVITY) is granted. The standard USE_FULL_SCREEN_INTENT path and setShowWhenLocked flags are accepted by the framework but still blocked by this gate. Expose a new CallkeepSpecialPermissions.backgroundActivityStart with a best-effort status read (reflection over the hidden AppOps op, Xiaomi-family only) and a deep link to the MIUI 'Other permissions' editor with fallback to app settings. Reports granted where the capability does not apply. WT-1349 * fix(android): report background-activity-start as unknown when unreadable The hidden MIUI AppOps op cannot be read on every build (greylisted on newer Android/HyperOS). Treating a failed read as denied told users the capability was off even after they enabled it, so the guidance never cleared. Return null (unknown) on read failure and map it to UNKNOWN, and accept MODE_DEFAULT and MODE_FOREGROUND as not-denied so a defer-to-policy state is not misreported. WT-1349 * fix(android): match Xiaomi family by substring, not exact equality Exact MANUFACTURER equality missed reported variants (e.g. 'Xiaomi Communications'). Use contains() for xiaomi/redmi/poco, matching the detection already used in CallDiagnostics, so the capability check applies on the same devices. WT-1349 * feat: expose xiaomi "showWhenLocked" permissions --------- Co-authored-by: Vladislav Komelkov <v.komelkov@webtrit.com>
1 parent d664409 commit 02cd4ce

9 files changed

Lines changed: 547 additions & 6 deletions

File tree

webtrit_callkeep/lib/src/webtrit_callkeep_permissions.dart

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,73 @@ class WebtritCallkeepPermissions {
5151
return platform.openFullScreenIntentSettings();
5252
}
5353

54+
/// Status of the OEM "display pop-up windows while running in background"
55+
/// capability (MIUI/HyperOS), which gates showing the incoming-call UI over
56+
/// the lock screen.
57+
///
58+
/// On non-Android platforms and web, returns
59+
/// [CallkeepSpecialPermissionStatus.granted] (the capability does not apply).
60+
Future<CallkeepSpecialPermissionStatus> getBackgroundActivityStartPermissionStatus() {
61+
if (kIsWeb) {
62+
return Future.value(CallkeepSpecialPermissionStatus.granted);
63+
}
64+
65+
if (!Platform.isAndroid) {
66+
return Future.value(CallkeepSpecialPermissionStatus.granted);
67+
}
68+
69+
return platform.getBackgroundActivityStartPermissionStatus();
70+
}
71+
72+
/// Attempts to open the OEM permissions screen hosting the "display pop-up
73+
/// windows while running in background" toggle.
74+
///
75+
/// On non-Android platforms and web, this call does nothing.
76+
Future<void> openBackgroundActivityStartSettings() {
77+
if (kIsWeb) {
78+
return Future.value();
79+
}
80+
81+
if (!Platform.isAndroid) {
82+
return Future.value();
83+
}
84+
85+
return platform.openBackgroundActivityStartSettings();
86+
}
87+
88+
/// Status of the OEM "show on lock screen" capability (MIUI/HyperOS), which
89+
/// gates showing the incoming-call UI over the lock screen.
90+
///
91+
/// On non-Android platforms and web, returns
92+
/// [CallkeepSpecialPermissionStatus.granted] (the capability does not apply).
93+
Future<CallkeepSpecialPermissionStatus> getShowWhenLockedPermissionStatus() {
94+
if (kIsWeb) {
95+
return Future.value(CallkeepSpecialPermissionStatus.granted);
96+
}
97+
98+
if (!Platform.isAndroid) {
99+
return Future.value(CallkeepSpecialPermissionStatus.granted);
100+
}
101+
102+
return platform.getShowWhenLockedPermissionStatus();
103+
}
104+
105+
/// Attempts to open the OEM permissions screen hosting the "show on lock
106+
/// screen" toggle.
107+
///
108+
/// On non-Android platforms and web, this call does nothing.
109+
Future<void> openShowWhenLockedSettings() {
110+
if (kIsWeb) {
111+
return Future.value();
112+
}
113+
114+
if (!Platform.isAndroid) {
115+
return Future.value();
116+
}
117+
118+
return platform.openShowWhenLockedSettings();
119+
}
120+
54121
/// Attempts to open the system settings screen for managing the app's permissions.
55122
// TODO(Serdun): Add support for iOS.
56123
Future<void> openSettings() {
@@ -129,10 +196,12 @@ extension CallkeepSpecialPermissionsExtension on CallkeepSpecialPermissions {
129196
/// If the permission is [CallkeepSpecialPermissions.fullScreenIntent], it checks the full screen intent permission status.
130197
/// Returns a [Future] that resolves to a [CallkeepSpecialPermissionStatus] indicating the status of the permission.
131198
Future<CallkeepSpecialPermissionStatus> status() async {
132-
if (this == CallkeepSpecialPermissions.fullScreenIntent) {
133-
final callkeepPermissions = WebtritCallkeepPermissions();
134-
return callkeepPermissions.getFullScreenIntentPermissionStatus();
199+
final callkeepPermissions = WebtritCallkeepPermissions();
200+
switch (this) {
201+
case CallkeepSpecialPermissions.fullScreenIntent:
202+
return callkeepPermissions.getFullScreenIntentPermissionStatus();
203+
case CallkeepSpecialPermissions.backgroundActivityStart:
204+
return callkeepPermissions.getBackgroundActivityStartPermissionStatus();
135205
}
136-
return CallkeepSpecialPermissionStatus.granted;
137206
}
138207
}

webtrit_callkeep_android/android/src/main/kotlin/com/webtrit/callkeep/Generated.kt

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1453,6 +1453,30 @@ interface PHostBackgroundPushNotificationIsolateApi {
14531453
interface PHostPermissionsApi {
14541454
fun getFullScreenIntentPermissionStatus(callback: (Result<PSpecialPermissionStatusTypeEnum>) -> Unit)
14551455
fun openFullScreenIntentSettings(callback: (Result<Unit>) -> Unit)
1456+
/**
1457+
* Status of the OEM "display pop-up windows while running in background"
1458+
* capability (MIUI/HyperOS `OP_BACKGROUND_START_ACTIVITY`), which gates
1459+
* showing the incoming-call Activity over the lock screen. Best-effort:
1460+
* reports granted on devices where the capability does not apply.
1461+
*/
1462+
fun getBackgroundActivityStartPermissionStatus(callback: (Result<PSpecialPermissionStatusTypeEnum>) -> Unit)
1463+
/**
1464+
* Opens the OEM permissions screen that hosts the "display pop-up windows
1465+
* while running in background" toggle, with a fallback to app settings.
1466+
*/
1467+
fun openBackgroundActivityStartSettings(callback: (Result<Unit>) -> Unit)
1468+
/**
1469+
* Status of the OEM "display pop-up windows while running in background"
1470+
* MIUI/HyperOS `OP_SHOW_WHEN_LOCKED` capability, which gates showing the
1471+
* incoming-call Activity over the lock screen. Best-effort: reports
1472+
* granted on devices where the capability does not apply.
1473+
*/
1474+
fun getShowWhenLockedPermissionStatus(callback: (Result<PSpecialPermissionStatusTypeEnum>) -> Unit)
1475+
/**
1476+
* Opens the OEM permissions screen that hosts the "show on lock screen"
1477+
* toggle, with a fallback to app settings.
1478+
*/
1479+
fun openShowWhenLockedSettings(callback: (Result<Unit>) -> Unit)
14561480
fun openSettings(callback: (Result<Unit>) -> Unit)
14571481
fun getBatteryMode(callback: (Result<PCallkeepAndroidBatteryMode>) -> Unit)
14581482
/**
@@ -1507,6 +1531,76 @@ interface PHostPermissionsApi {
15071531
channel.setMessageHandler(null)
15081532
}
15091533
}
1534+
run {
1535+
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.webtrit_callkeep_android.PHostPermissionsApi.getBackgroundActivityStartPermissionStatus$separatedMessageChannelSuffix", codec)
1536+
if (api != null) {
1537+
channel.setMessageHandler { _, reply ->
1538+
api.getBackgroundActivityStartPermissionStatus{ result: Result<PSpecialPermissionStatusTypeEnum> ->
1539+
val error = result.exceptionOrNull()
1540+
if (error != null) {
1541+
reply.reply(GeneratedPigeonUtils.wrapError(error))
1542+
} else {
1543+
val data = result.getOrNull()
1544+
reply.reply(GeneratedPigeonUtils.wrapResult(data))
1545+
}
1546+
}
1547+
}
1548+
} else {
1549+
channel.setMessageHandler(null)
1550+
}
1551+
}
1552+
run {
1553+
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.webtrit_callkeep_android.PHostPermissionsApi.openBackgroundActivityStartSettings$separatedMessageChannelSuffix", codec)
1554+
if (api != null) {
1555+
channel.setMessageHandler { _, reply ->
1556+
api.openBackgroundActivityStartSettings{ result: Result<Unit> ->
1557+
val error = result.exceptionOrNull()
1558+
if (error != null) {
1559+
reply.reply(GeneratedPigeonUtils.wrapError(error))
1560+
} else {
1561+
reply.reply(GeneratedPigeonUtils.wrapResult(null))
1562+
}
1563+
}
1564+
}
1565+
} else {
1566+
channel.setMessageHandler(null)
1567+
}
1568+
}
1569+
run {
1570+
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.webtrit_callkeep_android.PHostPermissionsApi.getShowWhenLockedPermissionStatus$separatedMessageChannelSuffix", codec)
1571+
if (api != null) {
1572+
channel.setMessageHandler { _, reply ->
1573+
api.getShowWhenLockedPermissionStatus{ result: Result<PSpecialPermissionStatusTypeEnum> ->
1574+
val error = result.exceptionOrNull()
1575+
if (error != null) {
1576+
reply.reply(GeneratedPigeonUtils.wrapError(error))
1577+
} else {
1578+
val data = result.getOrNull()
1579+
reply.reply(GeneratedPigeonUtils.wrapResult(data))
1580+
}
1581+
}
1582+
}
1583+
} else {
1584+
channel.setMessageHandler(null)
1585+
}
1586+
}
1587+
run {
1588+
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.webtrit_callkeep_android.PHostPermissionsApi.openShowWhenLockedSettings$separatedMessageChannelSuffix", codec)
1589+
if (api != null) {
1590+
channel.setMessageHandler { _, reply ->
1591+
api.openShowWhenLockedSettings{ result: Result<Unit> ->
1592+
val error = result.exceptionOrNull()
1593+
if (error != null) {
1594+
reply.reply(GeneratedPigeonUtils.wrapError(error))
1595+
} else {
1596+
reply.reply(GeneratedPigeonUtils.wrapResult(null))
1597+
}
1598+
}
1599+
}
1600+
} else {
1601+
channel.setMessageHandler(null)
1602+
}
1603+
}
15101604
run {
15111605
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.webtrit_callkeep_android.PHostPermissionsApi.openSettings$separatedMessageChannelSuffix", codec)
15121606
if (api != null) {

webtrit_callkeep_android/android/src/main/kotlin/com/webtrit/callkeep/PermissionsApi.kt

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,63 @@ class PermissionsApi(
5252
}
5353
}
5454

55+
/**
56+
* Reports the status of the OEM "display pop-up windows while running in
57+
* background" capability (MIUI/HyperOS), which gates showing the incoming
58+
* call UI over the lock screen. Best-effort; reports granted where the
59+
* capability does not apply.
60+
*/
61+
override fun getBackgroundActivityStartPermissionStatus(callback: (Result<PSpecialPermissionStatusTypeEnum>) -> Unit) {
62+
val status =
63+
when (PermissionsHelper(context).isBackgroundActivityStartGranted()) {
64+
true -> PSpecialPermissionStatusTypeEnum.GRANTED
65+
false -> PSpecialPermissionStatusTypeEnum.DENIED
66+
null -> PSpecialPermissionStatusTypeEnum.UNKNOWN
67+
}
68+
callback.invoke(Result.success(status))
69+
}
70+
71+
/**
72+
* Opens the OEM permissions screen hosting the "display pop-up windows while
73+
* running in background" toggle, with a fallback to app settings.
74+
*/
75+
override fun openBackgroundActivityStartSettings(callback: (Result<Unit>) -> Unit) {
76+
try {
77+
PermissionsHelper(context).launchBackgroundActivityStartSettings()
78+
callback.invoke(Result.success(Unit))
79+
} catch (e: Exception) {
80+
callback.invoke(Result.failure(e))
81+
}
82+
}
83+
84+
/**
85+
* Reports the status of the OEM "show on lock screen" capability
86+
* (MIUI/HyperOS), which gates showing the incoming call UI over the lock
87+
* screen. Best-effort; reports granted where the capability does not apply.
88+
*/
89+
override fun getShowWhenLockedPermissionStatus(callback: (Result<PSpecialPermissionStatusTypeEnum>) -> Unit) {
90+
val status =
91+
when (PermissionsHelper(context).isShowWhenLockedGranted()) {
92+
true -> PSpecialPermissionStatusTypeEnum.GRANTED
93+
false -> PSpecialPermissionStatusTypeEnum.DENIED
94+
null -> PSpecialPermissionStatusTypeEnum.UNKNOWN
95+
}
96+
callback.invoke(Result.success(status))
97+
}
98+
99+
/**
100+
* Opens the OEM permissions screen hosting the "show on lock screen"
101+
* toggle, with a fallback to app settings.
102+
*/
103+
override fun openShowWhenLockedSettings(callback: (Result<Unit>) -> Unit) {
104+
try {
105+
PermissionsHelper(context).launchShowWhenLockedSettings()
106+
callback.invoke(Result.success(Unit))
107+
} catch (e: Exception) {
108+
callback.invoke(Result.failure(e))
109+
}
110+
}
111+
55112
/**
56113
* Attempts to open the common system settings screen
57114
*/

0 commit comments

Comments
 (0)