Skip to content

Commit b851e61

Browse files
committed
fix: prevent crash on full-screen settings and guide sideload users past restricted settings
Wrap every system-settings launch so a screen a device doesn't ship (like the full-screen-intent page) falls back to the app info page instead of throwing ActivityNotFoundException. Add a note to the accessibility disclosure explaining how to get past Android's 'restricted setting' block for sideloaded apps, with a button straight to app info.
1 parent 59edea6 commit b851e61

2 files changed

Lines changed: 111 additions & 37 deletions

File tree

app/src/main/java/com/focusforceplus/app/ui/screens/blocker/DisclosureScreen.kt

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@ fun DisclosureScreen(
142142
"Anytime, in Android Settings > Accessibility > FocusForce+ App Blocker. " +
143143
"Your rules stay saved; blocking simply pauses.",
144144
)
145+
RestrictedSettingsCard(
146+
onOpenAppInfo = { PermissionHelper.openAppDetailsSettings(context) },
147+
)
145148
} else {
146149
DisclosureSection(
147150
"What this permission does",
@@ -234,6 +237,51 @@ private fun isGranted(context: android.content.Context, isAccessibility: Boolean
234237
if (isAccessibility) PermissionHelper.isAccessibilityServiceEnabled(context)
235238
else PermissionHelper.hasUsageStatsPermission(context)
236239

240+
/**
241+
* Sideload note: Android blocks the accessibility toggle for apps not installed
242+
* from the Play Store ("Restricted setting"). We can't change that, so we explain
243+
* the one-time unblock and offer a direct button to this app's info page.
244+
*/
245+
@Composable
246+
private fun RestrictedSettingsCard(onOpenAppInfo: () -> Unit) {
247+
Card(
248+
modifier = Modifier.fillMaxWidth(),
249+
shape = RoundedCornerShape(12.dp),
250+
colors = CardDefaults.cardColors(
251+
containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.12f),
252+
),
253+
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
254+
) {
255+
Column(
256+
modifier = Modifier.padding(14.dp),
257+
verticalArrangement = Arrangement.spacedBy(6.dp),
258+
) {
259+
Text(
260+
"Seeing \"Restricted setting\"?",
261+
style = MaterialTheme.typography.titleSmall,
262+
fontWeight = FontWeight.SemiBold,
263+
color = MaterialTheme.colorScheme.primary,
264+
)
265+
Text(
266+
"Because you installed FocusForce+ directly instead of from the Play " +
267+
"Store, Android blocks the accessibility toggle the first time. To " +
268+
"unblock it: open the app info page below, tap the three-dot menu in " +
269+
"the top corner, and choose \"Allow restricted settings\". Then come " +
270+
"back and turn the service on.",
271+
style = MaterialTheme.typography.bodyMedium,
272+
color = MaterialTheme.colorScheme.onSurface,
273+
)
274+
Button(
275+
onClick = onOpenAppInfo,
276+
shape = RoundedCornerShape(10.dp),
277+
modifier = Modifier.padding(top = 2.dp),
278+
) {
279+
Text("Open app info")
280+
}
281+
}
282+
}
283+
}
284+
237285
@Composable
238286
private fun DisclosureSection(title: String, body: String) {
239287
Card(

app/src/main/java/com/focusforceplus/app/util/PermissionHelper.kt

Lines changed: 63 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.focusforceplus.app.util
33
import android.app.AlarmManager
44
import android.app.AppOpsManager
55
import android.app.NotificationManager
6+
import android.content.ActivityNotFoundException
67
import android.content.ComponentName
78
import android.content.Context
89
import android.content.Intent
@@ -20,11 +21,13 @@ object PermissionHelper {
2021

2122
/** Opens the system page where the user can grant "Display over other apps". */
2223
fun openOverlaySettings(context: Context) {
23-
val intent = Intent(
24-
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
25-
Uri.parse("package:${context.packageName}"),
26-
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
27-
context.startActivity(intent)
24+
startSettingsSafely(
25+
context,
26+
Intent(
27+
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
28+
Uri.parse("package:${context.packageName}"),
29+
),
30+
)
2831
}
2932

3033
/** True if the app is allowed to launch full-screen intents (alarm-style overlays). */
@@ -34,13 +37,17 @@ object PermissionHelper {
3437
return nm.canUseFullScreenIntent()
3538
}
3639

37-
/** Opens the system page where the user can grant USE_FULL_SCREEN_INTENT. */
40+
/** Opens the system page where the user can grant USE_FULL_SCREEN_INTENT.
41+
* Not every device/OEM ships this dedicated screen, so we fall back gracefully. */
3842
fun openFullScreenIntentSettings(context: Context) {
3943
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
40-
val intent = Intent("android.settings.MANAGE_APP_USE_FULL_SCREEN_INTENTS")
41-
intent.data = Uri.parse("package:${context.packageName}")
42-
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
43-
context.startActivity(intent)
44+
startSettingsSafely(
45+
context,
46+
Intent("android.settings.MANAGE_APP_USE_FULL_SCREEN_INTENTS")
47+
.setData(Uri.parse("package:${context.packageName}")),
48+
Intent("android.settings.APP_NOTIFICATION_SETTINGS")
49+
.putExtra("android.provider.extra.APP_PACKAGE", context.packageName),
50+
)
4451
}
4552
}
4653

@@ -56,11 +63,11 @@ object PermissionHelper {
5663
/** Opens the system page where the user can grant SCHEDULE_EXACT_ALARM (Android 12 only). */
5764
fun openExactAlarmSettings(context: Context) {
5865
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
59-
val intent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM).apply {
60-
data = Uri.parse("package:${context.packageName}")
61-
flags = Intent.FLAG_ACTIVITY_NEW_TASK
62-
}
63-
context.startActivity(intent)
66+
startSettingsSafely(
67+
context,
68+
Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
69+
.setData(Uri.parse("package:${context.packageName}")),
70+
)
6471
}
6572
}
6673

@@ -93,10 +100,12 @@ object PermissionHelper {
93100
/** Opens the system accessibility settings (the service must be enabled there).
94101
* Only call this AFTER the in-app prominent disclosure has been confirmed. */
95102
fun openAccessibilitySettings(context: Context) {
96-
context.startActivity(
97-
Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
98-
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
99-
)
103+
startSettingsSafely(context, Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
104+
}
105+
106+
/** Opens this app's Android app-info page (where "Allow restricted settings" lives). */
107+
fun openAppDetailsSettings(context: Context) {
108+
startSettingsSafely(context, appDetailsIntent(context))
100109
}
101110

102111
/** True if Usage Access (PACKAGE_USAGE_STATS) has been granted. */
@@ -118,16 +127,12 @@ object PermissionHelper {
118127
/** Opens the system Usage Access settings.
119128
* Only call this AFTER the in-app prominent disclosure has been confirmed. */
120129
fun openUsageAccessSettings(context: Context) {
121-
val direct = Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)
122-
.setData(Uri.parse("package:${context.packageName}"))
123-
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
124-
runCatching { context.startActivity(direct) }.onFailure {
125-
// Some OEMs reject the package URI form; fall back to the list page.
126-
context.startActivity(
127-
Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)
128-
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
129-
)
130-
}
130+
startSettingsSafely(
131+
context,
132+
Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)
133+
.setData(Uri.parse("package:${context.packageName}")),
134+
Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS),
135+
)
131136
}
132137

133138
// ── Focus mode (Do Not Disturb) ───────────────────────────────────────────
@@ -140,10 +145,7 @@ object PermissionHelper {
140145

141146
/** Opens the system page where the user can grant DND access. */
142147
fun openDndAccessSettings(context: Context) {
143-
context.startActivity(
144-
Intent(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS)
145-
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
146-
)
148+
startSettingsSafely(context, Intent(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS))
147149
}
148150

149151
// ── Battery optimization ──────────────────────────────────────────────────
@@ -158,9 +160,33 @@ object PermissionHelper {
158160
* We deliberately use the non-prompting list intent: REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
159161
* is restricted and per compliance guide (3.5) not to be forced. */
160162
fun openBatteryOptimizationSettings(context: Context) {
161-
context.startActivity(
162-
Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
163-
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
164-
)
163+
startSettingsSafely(context, Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS))
164+
}
165+
166+
// ── Internals ──────────────────────────────────────────────────────────────
167+
168+
private fun appDetailsIntent(context: Context) = Intent(
169+
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
170+
Uri.parse("package:${context.packageName}"),
171+
)
172+
173+
/**
174+
* Tries each settings intent in turn, always falling back to the app-info page,
175+
* so a device that doesn't ship a particular settings screen never crashes the
176+
* app. Every candidate gets NEW_TASK since these are launched from non-activity
177+
* contexts too.
178+
*/
179+
private fun startSettingsSafely(context: Context, vararg candidates: Intent) {
180+
val all = candidates.toList() + appDetailsIntent(context)
181+
for (intent in all) {
182+
try {
183+
context.startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
184+
return
185+
} catch (_: ActivityNotFoundException) {
186+
// try the next candidate
187+
} catch (_: Exception) {
188+
// some OEMs throw SecurityException etc. — try the next candidate
189+
}
190+
}
165191
}
166192
}

0 commit comments

Comments
 (0)