Skip to content

Commit 3535221

Browse files
committed
fix: gracefully handle unsupported overlay permission on low-RAM devices
1 parent daaccab commit 3535221

File tree

7 files changed

+141
-47
lines changed

7 files changed

+141
-47
lines changed

app/src/main/java/com/itsaky/androidide/actions/build/DebugAction.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import android.content.ActivityNotFoundException
77
import android.content.Context
88
import android.content.Intent
99
import android.graphics.Color
10+
import android.net.Uri
1011
import android.os.Build
1112
import android.provider.Settings
1213
import android.text.SpannableStringBuilder
@@ -17,6 +18,7 @@ import androidx.core.content.ContextCompat.startForegroundService
1718
import androidx.core.view.setPadding
1819
import com.google.android.material.textview.MaterialTextView
1920
import com.itsaky.androidide.actions.ActionData
21+
import com.itsaky.androidide.activities.editor.EditorHandlerActivity
2022
import com.itsaky.androidide.activities.editor.HelpActivity
2123
import com.itsaky.androidide.idetooltips.TooltipTag
2224
import com.itsaky.androidide.lsp.api.ILanguageServerRegistry
@@ -26,6 +28,7 @@ import com.itsaky.androidide.projects.IProjectManager
2628
import com.itsaky.androidide.projects.isPluginProject
2729
import com.itsaky.androidide.resources.R
2830
import com.itsaky.androidide.utils.DialogUtils
31+
import com.itsaky.androidide.utils.PermissionsHelper
2932
import com.itsaky.androidide.utils.appendHtmlWithLinks
3033
import com.itsaky.androidide.utils.appendOrderedList
3134
import com.itsaky.androidide.utils.flashError
@@ -99,6 +102,15 @@ class DebugAction(
99102
return false
100103
}
101104

105+
val overlayState = withContext(Dispatchers.Main.immediate) {
106+
PermissionsHelper.getOverlayPermissionState(activity)
107+
}
108+
109+
if (overlayState != PermissionsHelper.OverlayPermissionState.GRANTED) {
110+
handleMissingOverlayPermission(activity, overlayState)
111+
return false
112+
}
113+
102114
if (!Shizuku.pingBinder()) {
103115
log.error("Shizuku service is not running")
104116
withContext(Dispatchers.Main.immediate) {
@@ -110,6 +122,32 @@ class DebugAction(
110122
return Shizuku.pingBinder()
111123
}
112124

125+
private suspend fun handleMissingOverlayPermission(
126+
activity: EditorHandlerActivity,
127+
state: PermissionsHelper.OverlayPermissionState
128+
) {
129+
withContext(Dispatchers.Main.immediate) {
130+
when (state) {
131+
PermissionsHelper.OverlayPermissionState.UNSUPPORTED -> {
132+
activity.flashError(activity.getString(R.string.permission_overlay_unsupported_hint))
133+
}
134+
PermissionsHelper.OverlayPermissionState.REQUESTABLE -> {
135+
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION).apply {
136+
putExtra(Settings.EXTRA_APP_PACKAGE, activity.packageName)
137+
setData(Uri.fromParts("package", activity.packageName, null))
138+
}
139+
try {
140+
activity.startActivity(intent)
141+
} catch (e: Exception) {
142+
log.error("Failed to launch overlay settings", e)
143+
activity.flashError(activity.getString(R.string.err_no_activity_to_handle_action, Settings.ACTION_MANAGE_OVERLAY_PERMISSION))
144+
}
145+
}
146+
else -> {}
147+
}
148+
}
149+
}
150+
113151
@RequiresApi(Build.VERSION_CODES.R)
114152
private fun showPairingDialog(context: Context): AlertDialog? {
115153
val launchHelp = { url: String ->

app/src/main/java/com/itsaky/androidide/adapters/onboarding/OnboardingPermissionsAdapter.kt

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ import android.content.res.ColorStateList
2121
import android.view.LayoutInflater
2222
import android.view.ViewGroup
2323
import androidx.core.content.ContextCompat
24+
import androidx.core.graphics.ColorUtils
2425
import androidx.recyclerview.widget.RecyclerView
2526
import com.blankj.utilcode.util.SizeUtils
2627
import com.google.android.material.button.MaterialButton
28+
import com.google.android.material.color.MaterialColors
2729
import com.itsaky.androidide.R
2830
import com.itsaky.androidide.databinding.LayoutOnboardingPermissionItemBinding
2931
import com.itsaky.androidide.models.OnboardingPermissionItem
@@ -36,7 +38,12 @@ class OnboardingPermissionsAdapter(private val permissions: List<OnboardingPermi
3638
RecyclerView.Adapter<OnboardingPermissionsAdapter.ViewHolder>() {
3739

3840
class ViewHolder(val binding: LayoutOnboardingPermissionItemBinding) :
39-
RecyclerView.ViewHolder(binding.root)
41+
RecyclerView.ViewHolder(binding.root) {
42+
val titleColor: Int = MaterialColors.getColor(binding.root, R.attr.colorOnSurface)
43+
val descriptionColor: Int = MaterialColors.getColor(binding.root, R.attr.colorOnSurfaceVariant)
44+
val disabledTitleColor: Int = ColorUtils.setAlphaComponent(titleColor, (255 * 0.38f).toInt())
45+
val disabledDescriptionColor: Int = ColorUtils.setAlphaComponent(descriptionColor, (255 * 0.38f).toInt())
46+
}
4047

4148
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
4249
return ViewHolder(
@@ -47,10 +54,23 @@ class OnboardingPermissionsAdapter(private val permissions: List<OnboardingPermi
4754
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
4855
val binding = holder.binding
4956
val permission = permissions[position]
57+
val context = binding.root.context
5058

5159
binding.infoContent.apply {
5260
title.setText(permission.title)
5361
description.setText(permission.description)
62+
title.setTextColor(if (permission.isSupportedOnDevice) holder.titleColor else holder.disabledTitleColor)
63+
description.setTextColor(if (permission.isSupportedOnDevice) holder.descriptionColor else holder.disabledDescriptionColor)
64+
}
65+
66+
binding.grantButton.apply {
67+
isEnabled = permission.isSupportedOnDevice
68+
text = context.getString(R.string.title_grant)
69+
icon = null
70+
iconTint = null
71+
iconGravity = MaterialButton.ICON_GRAVITY_TEXT_START
72+
iconPadding = 0
73+
iconSize = 0
5474
}
5575

5676
binding.grantButton.setOnClickListener {
@@ -61,9 +81,9 @@ class OnboardingPermissionsAdapter(private val permissions: List<OnboardingPermi
6181
binding.grantButton.apply {
6282
isEnabled = false
6383
text = ""
64-
icon = ContextCompat.getDrawable(binding.root.context, R.drawable.ic_ok)
84+
icon = ContextCompat.getDrawable(context, R.drawable.ic_ok)
6585
iconTint = ColorStateList.valueOf(
66-
ContextCompat.getColor(binding.root.context, R.color.green_500))
86+
ContextCompat.getColor(context, R.color.green_500))
6787
iconGravity = MaterialButton.ICON_GRAVITY_TEXT_TOP
6888
iconPadding = 0
6989
iconSize = SizeUtils.dp2px(28f)
@@ -74,4 +94,4 @@ class OnboardingPermissionsAdapter(private val permissions: List<OnboardingPermi
7494
override fun getItemCount(): Int {
7595
return permissions.size
7696
}
77-
}
97+
}

app/src/main/java/com/itsaky/androidide/fragments/onboarding/PermissionsFragment.kt

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ class PermissionsFragment :
114114
permissionsBinding?.let { b ->
115115
recyclerView = b.onboardingItems
116116
finishButton = b.finishInstallationButton
117-
pulseAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.pulse_animation)
117+
pulseAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.pulse_animation)
118118

119119
b.onboardingItems.adapter = createAdapter()
120120

@@ -217,17 +217,19 @@ class PermissionsFragment :
217217
viewModel.onPermissionsUpdated(allGranted)
218218
}
219219

220-
private fun handlePostOverlayPermissionState() {
221-
if (!awaitingOverlayGrantResult) {
222-
return
223-
}
224-
awaitingOverlayGrantResult = false
225-
if (PermissionsHelper.canDrawOverlays(requireContext())) {
226-
return
227-
}
228-
flashError(getString(R.string.permission_overlay_restricted_settings_hint))
229-
openAppInfoSettings()
230-
}
220+
private fun handlePostOverlayPermissionState() {
221+
if (!awaitingOverlayGrantResult) {
222+
return
223+
}
224+
awaitingOverlayGrantResult = false
225+
226+
if (PermissionsHelper.canDrawOverlays(requireContext())) {
227+
return
228+
}
229+
230+
flashError(getString(R.string.permission_overlay_restricted_settings_hint))
231+
requestSettingsTogglePermission(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
232+
}
231233

232234
private fun startIdeSetup() {
233235
val shouldProceed = viewModel.checkStorageAndNotify(requireContext())
@@ -294,13 +296,19 @@ class PermissionsFragment :
294296
}
295297
}
296298

297-
private fun requestOverlayPermission() {
298-
awaitingOverlayGrantResult = requestSettingsTogglePermission(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
299-
}
300-
301-
private fun openAppInfoSettings() {
302-
requestSettingsTogglePermission(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
303-
}
299+
private fun requestOverlayPermission() {
300+
val state = PermissionsHelper.getOverlayPermissionState(requireContext())
301+
302+
when (state) {
303+
PermissionsHelper.OverlayPermissionState.UNSUPPORTED -> {
304+
flashError(getString(R.string.permission_overlay_unsupported_hint))
305+
}
306+
PermissionsHelper.OverlayPermissionState.REQUESTABLE -> {
307+
awaitingOverlayGrantResult = requestSettingsTogglePermission(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
308+
}
309+
PermissionsHelper.OverlayPermissionState.GRANTED -> {}
310+
}
311+
}
304312

305313
private fun requestStoragePermission() {
306314
if (isAtLeastR()) {

app/src/main/java/com/itsaky/androidide/models/OnboardingPermissionItem.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,6 @@ data class OnboardingPermissionItem(
3232
val description: Int,
3333
var isGranted: Boolean,
3434

35-
var isOptional: Boolean = false
36-
)
35+
var isOptional: Boolean = false,
36+
var isSupportedOnDevice: Boolean = true
37+
)

app/src/main/java/com/itsaky/androidide/services/debug/DebugOverlayManager.kt

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import android.view.LayoutInflater
88
import android.view.MotionEvent
99
import android.view.ViewConfiguration
1010
import android.view.WindowManager
11+
import android.widget.Toast
1112
import android.provider.Settings
1213
import androidx.core.content.ContextCompat
1314
import com.itsaky.androidide.R
@@ -16,7 +17,7 @@ import com.itsaky.androidide.actions.ActionsRegistry
1617
import com.itsaky.androidide.databinding.DebuggerActionsWindowBinding
1718
import com.itsaky.androidide.idetooltips.TooltipManager
1819
import com.itsaky.androidide.idetooltips.TooltipTag
19-
import com.itsaky.androidide.utils.flashError
20+
import com.itsaky.androidide.utils.PermissionsHelper
2021
import org.slf4j.LoggerFactory
2122
import kotlin.math.abs
2223

@@ -111,9 +112,18 @@ class DebugOverlayManager private constructor(
111112
return
112113
}
113114

114-
if (!Settings.canDrawOverlays(binding.root.context)) {
115+
val ctx = binding.root.context
116+
117+
if (!Settings.canDrawOverlays(ctx)) {
115118
logger.warn("Overlay permission denied. Skipping debugger overlay window.")
116-
flashError(binding.root.context.getString(R.string.permission_overlay_restricted_settings_hint))
119+
120+
val state = PermissionsHelper.getOverlayPermissionState(ctx)
121+
val message = if (state == PermissionsHelper.OverlayPermissionState.UNSUPPORTED) {
122+
ctx.getString(R.string.permission_overlay_unsupported_hint)
123+
} else {
124+
ctx.getString(R.string.permission_overlay_restricted_settings_hint)
125+
}
126+
Toast.makeText(ctx, message, Toast.LENGTH_LONG).show()
117127
return
118128
}
119129

@@ -139,10 +149,10 @@ class DebugOverlayManager private constructor(
139149
}
140150
}
141151

142-
fun refreshActions() {
143-
// noinspection NotifyDataSetChanged
144-
binding.actions.adapter?.notifyDataSetChanged()
145-
}
152+
fun refreshActions() {
153+
// noinspection NotifyDataSetChanged
154+
binding.actions.adapter?.notifyDataSetChanged()
155+
}
146156

147157
companion object {
148158

@@ -172,9 +182,9 @@ class DebugOverlayManager private constructor(
172182
layout.actions.adapter = adapter
173183

174184
return DebugOverlayManager(
175-
windowManager = windowManager,
176-
binding = layout,
185+
windowManager = windowManager,
186+
binding = layout,
177187
)
178188
}
179189
}
180-
}
190+
}

app/src/main/java/com/itsaky/androidide/utils/PermissionsHelper.kt

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.itsaky.androidide.utils
22

33
import android.Manifest
4+
import android.app.ActivityManager
45
import android.content.Context
56
import android.content.pm.PackageManager
67
import android.os.Build
@@ -16,8 +17,29 @@ import com.itsaky.androidide.models.OnboardingPermissionItem
1617
*/
1718
object PermissionsHelper {
1819

20+
enum class OverlayPermissionState {
21+
GRANTED,
22+
REQUESTABLE,
23+
UNSUPPORTED,
24+
}
25+
26+
fun canDrawOverlays(context: Context): Boolean = Settings.canDrawOverlays(context)
27+
28+
fun getOverlayPermissionState(context: Context): OverlayPermissionState {
29+
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager
30+
val isLowRamDevice = activityManager?.isLowRamDevice ?: false
31+
32+
return when {
33+
canDrawOverlays(context) -> OverlayPermissionState.GRANTED
34+
isLowRamDevice -> OverlayPermissionState.UNSUPPORTED
35+
else -> OverlayPermissionState.REQUESTABLE
36+
}
37+
}
38+
1939
fun getRequiredPermissions(context: Context): List<OnboardingPermissionItem> {
2040
val permissions = mutableListOf<OnboardingPermissionItem>()
41+
val overlayState = getOverlayPermissionState(context)
42+
val isOverlaySupported = overlayState != OverlayPermissionState.UNSUPPORTED
2143

2244
if (isAtLeastT()) {
2345
permissions.add(
@@ -52,9 +74,11 @@ object PermissionsHelper {
5274
OnboardingPermissionItem(
5375
Manifest.permission.SYSTEM_ALERT_WINDOW,
5476
R.string.permission_title_overlay_window,
55-
R.string.permission_desc_overlay_window,
56-
canDrawOverlays(context),
57-
),
77+
if (isOverlaySupported) R.string.permission_desc_overlay_window else R.string.permission_overlay_unsupported_hint,
78+
overlayState == OverlayPermissionState.GRANTED,
79+
isOptional = !isOverlaySupported,
80+
isSupportedOnDevice = isOverlaySupported
81+
)
5882
)
5983

6084
return permissions
@@ -65,14 +89,9 @@ object PermissionsHelper {
6589
fun canPostNotifications(context: Context) =
6690
isPermissionGranted(context, Manifest.permission.POST_NOTIFICATIONS)
6791

68-
69-
fun canDrawOverlays(context: Context): Boolean = Settings.canDrawOverlays(context)
70-
71-
7292
fun areAllPermissionsGranted(context: Context): Boolean =
7393
getRequiredPermissions(context).all { it.isOptional || it.isGranted }
7494

75-
7695
fun isStoragePermissionGranted(context: Context): Boolean {
7796
if (isAtLeastR()) {
7897
return Environment.isExternalStorageManager()
@@ -87,11 +106,9 @@ object PermissionsHelper {
87106
)
88107
}
89108

90-
91109
fun canRequestPackageInstalls(context: Context): Boolean =
92110
context.packageManager.canRequestPackageInstalls()
93111

94-
95112
fun isPermissionGranted(
96113
context: Context,
97114
permission: String,
@@ -102,12 +119,11 @@ object PermissionsHelper {
102119
else -> checkSelfPermission(context, permission)
103120
}
104121

105-
106122
fun checkSelfPermission(
107123
context: Context,
108124
permission: String,
109125
): Boolean = ActivityCompat.checkSelfPermission(
110126
context,
111127
permission,
112128
) == PackageManager.PERMISSION_GRANTED
113-
}
129+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,7 @@
717717
<string name="permission_desc_install_packages">Allow Code on the Go to install the apps that you build on this device.</string>
718718
<string name="permission_title_overlay_window">Floating debugger</string>
719719
<string name="permission_desc_overlay_window">Allow Code on the Go to display floating debugger controls to inspect your code while it runs.</string>
720+
<string name="permission_overlay_unsupported_hint">Floating debugger is not available on this device.</string>
720721
<string name="permission_overlay_restricted_settings_hint">If you can\'t enable this permission, display App info for Code on the Go, tap the three-dot menu, and enable restricted settings.</string>
721722
<string name="permission_title_notifications">Notifications</string>
722723
<string name="permission_desc_notifications">Allow Code on the Go to display notifications.</string>

0 commit comments

Comments
 (0)