From daa29cd2899e7c99e099730a0200a011d014706f Mon Sep 17 00:00:00 2001 From: John Trujillo Date: Tue, 7 Apr 2026 13:27:59 -0500 Subject: [PATCH 1/2] fix: gracefully handle unsupported overlay permission on low-RAM devices --- .../androidide/actions/build/DebugAction.kt | 38 +++++++++++++++ .../OnboardingPermissionsAdapter.kt | 28 +++++++++-- .../onboarding/PermissionsFragment.kt | 46 +++++++++++-------- .../models/OnboardingPermissionItem.kt | 5 +- .../services/debug/DebugOverlayManager.kt | 30 ++++++++---- .../androidide/utils/PermissionsHelper.kt | 40 +++++++++++----- resources/src/main/res/values/strings.xml | 1 + 7 files changed, 141 insertions(+), 47 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/actions/build/DebugAction.kt b/app/src/main/java/com/itsaky/androidide/actions/build/DebugAction.kt index 9e0642867a..37b5f61899 100644 --- a/app/src/main/java/com/itsaky/androidide/actions/build/DebugAction.kt +++ b/app/src/main/java/com/itsaky/androidide/actions/build/DebugAction.kt @@ -7,6 +7,7 @@ import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.graphics.Color +import android.net.Uri import android.os.Build import android.provider.Settings import android.text.SpannableStringBuilder @@ -17,6 +18,7 @@ import androidx.core.content.ContextCompat.startForegroundService import androidx.core.view.setPadding import com.google.android.material.textview.MaterialTextView import com.itsaky.androidide.actions.ActionData +import com.itsaky.androidide.activities.editor.EditorHandlerActivity import com.itsaky.androidide.activities.editor.HelpActivity import com.itsaky.androidide.idetooltips.TooltipTag import com.itsaky.androidide.lsp.api.ILanguageServerRegistry @@ -26,6 +28,7 @@ import com.itsaky.androidide.projects.IProjectManager import com.itsaky.androidide.projects.isPluginProject import com.itsaky.androidide.resources.R import com.itsaky.androidide.utils.DialogUtils +import com.itsaky.androidide.utils.PermissionsHelper import com.itsaky.androidide.utils.appendHtmlWithLinks import com.itsaky.androidide.utils.appendOrderedList import com.itsaky.androidide.utils.flashError @@ -99,6 +102,15 @@ class DebugAction( return false } + val overlayState = withContext(Dispatchers.Main.immediate) { + PermissionsHelper.getOverlayPermissionState(activity) + } + + if (overlayState != PermissionsHelper.OverlayPermissionState.GRANTED) { + handleMissingOverlayPermission(activity, overlayState) + return false + } + if (!Shizuku.pingBinder()) { log.error("Shizuku service is not running") withContext(Dispatchers.Main.immediate) { @@ -110,6 +122,32 @@ class DebugAction( return Shizuku.pingBinder() } + private suspend fun handleMissingOverlayPermission( + activity: EditorHandlerActivity, + state: PermissionsHelper.OverlayPermissionState + ) { + withContext(Dispatchers.Main.immediate) { + when (state) { + PermissionsHelper.OverlayPermissionState.UNSUPPORTED -> { + activity.flashError(activity.getString(R.string.permission_overlay_unsupported_hint)) + } + PermissionsHelper.OverlayPermissionState.REQUESTABLE -> { + val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION).apply { + putExtra(Settings.EXTRA_APP_PACKAGE, activity.packageName) + setData(Uri.fromParts("package", activity.packageName, null)) + } + try { + activity.startActivity(intent) + } catch (e: Exception) { + log.error("Failed to launch overlay settings", e) + activity.flashError(activity.getString(R.string.err_no_activity_to_handle_action, Settings.ACTION_MANAGE_OVERLAY_PERMISSION)) + } + } + else -> {} + } + } + } + @RequiresApi(Build.VERSION_CODES.R) private fun showPairingDialog(context: Context): AlertDialog? { val launchHelp = { url: String -> diff --git a/app/src/main/java/com/itsaky/androidide/adapters/onboarding/OnboardingPermissionsAdapter.kt b/app/src/main/java/com/itsaky/androidide/adapters/onboarding/OnboardingPermissionsAdapter.kt index 7affdf8b89..2fdc7505b7 100644 --- a/app/src/main/java/com/itsaky/androidide/adapters/onboarding/OnboardingPermissionsAdapter.kt +++ b/app/src/main/java/com/itsaky/androidide/adapters/onboarding/OnboardingPermissionsAdapter.kt @@ -21,9 +21,11 @@ import android.content.res.ColorStateList import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.content.ContextCompat +import androidx.core.graphics.ColorUtils import androidx.recyclerview.widget.RecyclerView import com.blankj.utilcode.util.SizeUtils import com.google.android.material.button.MaterialButton +import com.google.android.material.color.MaterialColors import com.itsaky.androidide.R import com.itsaky.androidide.databinding.LayoutOnboardingPermissionItemBinding import com.itsaky.androidide.models.OnboardingPermissionItem @@ -36,7 +38,12 @@ class OnboardingPermissionsAdapter(private val permissions: List() { class ViewHolder(val binding: LayoutOnboardingPermissionItemBinding) : - RecyclerView.ViewHolder(binding.root) + RecyclerView.ViewHolder(binding.root) { + val titleColor: Int = MaterialColors.getColor(binding.root, R.attr.colorOnSurface) + val descriptionColor: Int = MaterialColors.getColor(binding.root, R.attr.colorOnSurfaceVariant) + val disabledTitleColor: Int = ColorUtils.setAlphaComponent(titleColor, (255 * 0.38f).toInt()) + val disabledDescriptionColor: Int = ColorUtils.setAlphaComponent(descriptionColor, (255 * 0.38f).toInt()) + } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder( @@ -47,10 +54,23 @@ class OnboardingPermissionsAdapter(private val permissions: List recyclerView = b.onboardingItems finishButton = b.finishInstallationButton - pulseAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.pulse_animation) + pulseAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.pulse_animation) b.onboardingItems.adapter = createAdapter() @@ -217,17 +217,19 @@ class PermissionsFragment : viewModel.onPermissionsUpdated(allGranted) } - private fun handlePostOverlayPermissionState() { - if (!awaitingOverlayGrantResult) { - return - } - awaitingOverlayGrantResult = false - if (PermissionsHelper.canDrawOverlays(requireContext())) { - return - } - flashError(getString(R.string.permission_overlay_restricted_settings_hint)) - openAppInfoSettings() - } + private fun handlePostOverlayPermissionState() { + if (!awaitingOverlayGrantResult) { + return + } + awaitingOverlayGrantResult = false + + if (PermissionsHelper.canDrawOverlays(requireContext())) { + return + } + + flashError(getString(R.string.permission_overlay_restricted_settings_hint)) + requestSettingsTogglePermission(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + } private fun startIdeSetup() { val shouldProceed = viewModel.checkStorageAndNotify(requireContext()) @@ -294,13 +296,19 @@ class PermissionsFragment : } } - private fun requestOverlayPermission() { - awaitingOverlayGrantResult = requestSettingsTogglePermission(Settings.ACTION_MANAGE_OVERLAY_PERMISSION) - } - - private fun openAppInfoSettings() { - requestSettingsTogglePermission(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) - } + private fun requestOverlayPermission() { + val state = PermissionsHelper.getOverlayPermissionState(requireContext()) + + when (state) { + PermissionsHelper.OverlayPermissionState.UNSUPPORTED -> { + flashError(getString(R.string.permission_overlay_unsupported_hint)) + } + PermissionsHelper.OverlayPermissionState.REQUESTABLE -> { + awaitingOverlayGrantResult = requestSettingsTogglePermission(Settings.ACTION_MANAGE_OVERLAY_PERMISSION) + } + PermissionsHelper.OverlayPermissionState.GRANTED -> {} + } + } private fun requestStoragePermission() { if (isAtLeastR()) { diff --git a/app/src/main/java/com/itsaky/androidide/models/OnboardingPermissionItem.kt b/app/src/main/java/com/itsaky/androidide/models/OnboardingPermissionItem.kt index b53273a950..d0c86b8d1c 100644 --- a/app/src/main/java/com/itsaky/androidide/models/OnboardingPermissionItem.kt +++ b/app/src/main/java/com/itsaky/androidide/models/OnboardingPermissionItem.kt @@ -32,5 +32,6 @@ data class OnboardingPermissionItem( val description: Int, var isGranted: Boolean, - var isOptional: Boolean = false -) \ No newline at end of file + var isOptional: Boolean = false, + var isSupportedOnDevice: Boolean = true +) diff --git a/app/src/main/java/com/itsaky/androidide/services/debug/DebugOverlayManager.kt b/app/src/main/java/com/itsaky/androidide/services/debug/DebugOverlayManager.kt index 7a3cc9a23a..6f18a1c6c7 100644 --- a/app/src/main/java/com/itsaky/androidide/services/debug/DebugOverlayManager.kt +++ b/app/src/main/java/com/itsaky/androidide/services/debug/DebugOverlayManager.kt @@ -8,6 +8,7 @@ import android.view.LayoutInflater import android.view.MotionEvent import android.view.ViewConfiguration import android.view.WindowManager +import android.widget.Toast import android.provider.Settings import androidx.core.content.ContextCompat import com.itsaky.androidide.R @@ -16,7 +17,7 @@ import com.itsaky.androidide.actions.ActionsRegistry import com.itsaky.androidide.databinding.DebuggerActionsWindowBinding import com.itsaky.androidide.idetooltips.TooltipManager import com.itsaky.androidide.idetooltips.TooltipTag -import com.itsaky.androidide.utils.flashError +import com.itsaky.androidide.utils.PermissionsHelper import org.slf4j.LoggerFactory import kotlin.math.abs @@ -111,9 +112,18 @@ class DebugOverlayManager private constructor( return } - if (!Settings.canDrawOverlays(binding.root.context)) { + val ctx = binding.root.context + + if (!Settings.canDrawOverlays(ctx)) { logger.warn("Overlay permission denied. Skipping debugger overlay window.") - flashError(binding.root.context.getString(R.string.permission_overlay_restricted_settings_hint)) + + val state = PermissionsHelper.getOverlayPermissionState(ctx) + val message = if (state == PermissionsHelper.OverlayPermissionState.UNSUPPORTED) { + ctx.getString(R.string.permission_overlay_unsupported_hint) + } else { + ctx.getString(R.string.permission_overlay_restricted_settings_hint) + } + Toast.makeText(ctx, message, Toast.LENGTH_LONG).show() return } @@ -139,10 +149,10 @@ class DebugOverlayManager private constructor( } } - fun refreshActions() { - // noinspection NotifyDataSetChanged - binding.actions.adapter?.notifyDataSetChanged() - } + fun refreshActions() { + // noinspection NotifyDataSetChanged + binding.actions.adapter?.notifyDataSetChanged() + } companion object { @@ -172,9 +182,9 @@ class DebugOverlayManager private constructor( layout.actions.adapter = adapter return DebugOverlayManager( - windowManager = windowManager, - binding = layout, + windowManager = windowManager, + binding = layout, ) } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/itsaky/androidide/utils/PermissionsHelper.kt b/app/src/main/java/com/itsaky/androidide/utils/PermissionsHelper.kt index 0311b016bf..5bc29ec3c7 100644 --- a/app/src/main/java/com/itsaky/androidide/utils/PermissionsHelper.kt +++ b/app/src/main/java/com/itsaky/androidide/utils/PermissionsHelper.kt @@ -1,6 +1,7 @@ package com.itsaky.androidide.utils import android.Manifest +import android.app.ActivityManager import android.content.Context import android.content.pm.PackageManager import android.os.Build @@ -16,8 +17,29 @@ import com.itsaky.androidide.models.OnboardingPermissionItem */ object PermissionsHelper { + enum class OverlayPermissionState { + GRANTED, + REQUESTABLE, + UNSUPPORTED, + } + + fun canDrawOverlays(context: Context): Boolean = Settings.canDrawOverlays(context) + + fun getOverlayPermissionState(context: Context): OverlayPermissionState { + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager + val isLowRamDevice = activityManager?.isLowRamDevice ?: false + + return when { + canDrawOverlays(context) -> OverlayPermissionState.GRANTED + isLowRamDevice -> OverlayPermissionState.UNSUPPORTED + else -> OverlayPermissionState.REQUESTABLE + } + } + fun getRequiredPermissions(context: Context): List { val permissions = mutableListOf() + val overlayState = getOverlayPermissionState(context) + val isOverlaySupported = overlayState != OverlayPermissionState.UNSUPPORTED if (isAtLeastT()) { permissions.add( @@ -52,9 +74,11 @@ object PermissionsHelper { OnboardingPermissionItem( Manifest.permission.SYSTEM_ALERT_WINDOW, R.string.permission_title_overlay_window, - R.string.permission_desc_overlay_window, - canDrawOverlays(context), - ), + if (isOverlaySupported) R.string.permission_desc_overlay_window else R.string.permission_overlay_unsupported_hint, + overlayState == OverlayPermissionState.GRANTED, + isOptional = !isOverlaySupported, + isSupportedOnDevice = isOverlaySupported + ) ) return permissions @@ -65,14 +89,9 @@ object PermissionsHelper { fun canPostNotifications(context: Context) = isPermissionGranted(context, Manifest.permission.POST_NOTIFICATIONS) - - fun canDrawOverlays(context: Context): Boolean = Settings.canDrawOverlays(context) - - fun areAllPermissionsGranted(context: Context): Boolean = getRequiredPermissions(context).all { it.isOptional || it.isGranted } - fun isStoragePermissionGranted(context: Context): Boolean { if (isAtLeastR()) { return Environment.isExternalStorageManager() @@ -87,11 +106,9 @@ object PermissionsHelper { ) } - fun canRequestPackageInstalls(context: Context): Boolean = context.packageManager.canRequestPackageInstalls() - fun isPermissionGranted( context: Context, permission: String, @@ -102,7 +119,6 @@ object PermissionsHelper { else -> checkSelfPermission(context, permission) } - fun checkSelfPermission( context: Context, permission: String, @@ -110,4 +126,4 @@ object PermissionsHelper { context, permission, ) == PackageManager.PERMISSION_GRANTED -} \ No newline at end of file +} diff --git a/resources/src/main/res/values/strings.xml b/resources/src/main/res/values/strings.xml index 3a67d9d109..5e226b42e8 100644 --- a/resources/src/main/res/values/strings.xml +++ b/resources/src/main/res/values/strings.xml @@ -717,6 +717,7 @@ Allow Code on the Go to install the apps that you build on this device. Floating debugger Allow Code on the Go to display floating debugger controls to inspect your code while it runs. + Floating debugger is not available on this device. If you can\'t enable this permission, display App info for Code on the Go, tap the three-dot menu, and enable restricted settings. Notifications Allow Code on the Go to display notifications. From 74bbdb67c51dae1ce10fb994d89dc15d65cdbe67 Mon Sep 17 00:00:00 2001 From: John Trujillo Date: Tue, 7 Apr 2026 15:40:57 -0500 Subject: [PATCH 2/2] fix: permission condition --- .../androidide/utils/PermissionsHelper.kt | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/utils/PermissionsHelper.kt b/app/src/main/java/com/itsaky/androidide/utils/PermissionsHelper.kt index 5bc29ec3c7..f5822bc5db 100644 --- a/app/src/main/java/com/itsaky/androidide/utils/PermissionsHelper.kt +++ b/app/src/main/java/com/itsaky/androidide/utils/PermissionsHelper.kt @@ -3,6 +3,7 @@ package com.itsaky.androidide.utils import android.Manifest import android.app.ActivityManager import android.content.Context +import android.content.Intent import android.content.pm.PackageManager import android.os.Build import android.os.Environment @@ -11,6 +12,7 @@ import androidx.annotation.RequiresApi import androidx.core.app.ActivityCompat import com.itsaky.androidide.R import com.itsaky.androidide.models.OnboardingPermissionItem +import androidx.core.net.toUri /** * @author Akash Yadav @@ -26,13 +28,27 @@ object PermissionsHelper { fun canDrawOverlays(context: Context): Boolean = Settings.canDrawOverlays(context) fun getOverlayPermissionState(context: Context): OverlayPermissionState { + if (canDrawOverlays(context)) { + return OverlayPermissionState.GRANTED + } + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager - val isLowRamDevice = activityManager?.isLowRamDevice ?: false + val isLowRamAndModernAndroid = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && activityManager?.isLowRamDevice == true + + if (isLowRamAndModernAndroid) { + return OverlayPermissionState.UNSUPPORTED + } + + val intent = Intent( + Settings.ACTION_MANAGE_OVERLAY_PERMISSION, + "package:${context.packageName}".toUri() + ) + val canResolveIntent = intent.resolveActivity(context.packageManager) != null - return when { - canDrawOverlays(context) -> OverlayPermissionState.GRANTED - isLowRamDevice -> OverlayPermissionState.UNSUPPORTED - else -> OverlayPermissionState.REQUESTABLE + return if (canResolveIntent) { + OverlayPermissionState.REQUESTABLE + } else { + OverlayPermissionState.UNSUPPORTED } }