{"record":{"id":"c84867ca57c5c299","repo":"microg/GmsCore","slug":"app-op-denied","errorCode":null,"errorMessage":"app op denied","messagePattern":"app op denied","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt","lineNumber":471,"sourceCode":"\n            fun check() {\n                if (!context.checkAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) throw RuntimeException(\"Lack of permission\")\n                if (effectiveGranularity > permissionGranularity) throw RuntimeException(\"Lack of permission\")\n                if (timePendingMillis < 0) throw RuntimeException(\"duration limit reached (active for ${(SystemClock.elapsedRealtime() - start).formatDuration()}, duration ${request.durationMillis.formatDuration()})\")\n                if (updatesPending <= 0) throw RuntimeException(\"max updates reached\")\n                if (callback?.asBinder()?.isBinderAlive == false) throw RuntimeException(\"Binder died\")\n            }\n\n            fun processNewLocation(location: Location): Boolean {\n                check()\n                if (lastLocation != null && location.elapsedMillis - lastLocation!!.elapsedMillis < request.minUpdateIntervalMillis) return false\n                if (lastLocation != null && location.distanceTo(lastLocation!!) < request.minUpdateDistanceMeters) return false\n                if (lastLocation == location) return false\n                val returnedLocation = if (effectiveGranularity > permissionGranularity) {\n                    throw RuntimeException(\"Lack of permission\")\n                } else {\n                    if (!context.noteAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) {\n                        throw RuntimeException(\"app op denied\")\n                    } else if (clientIdentity.isSelfProcess()) {\n                        Location(location)\n                    } else {\n                        Location(location).apply { provider = \"fused\" }\n                    }\n                }\n                val result = LocationResult.create(listOf(returnedLocation))\n                callback?.onLocationResult(result)\n                pendingIntent?.send(context, 0, Intent().apply { putExtra(LocationResult.EXTRA_LOCATION_RESULT, result) })\n                if (request.maxUpdates != Int.MAX_VALUE) updates++\n                check()\n                return true\n            }\n\n            init {\n                require(callback != null || pendingIntent != null)\n            }\n        }","sourceCodeStart":453,"sourceCodeEnd":489,"githubUrl":"https://github.com/microg/GmsCore/blob/157c9d86ac46c195a86c2f15ab55c84036223f95/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt#L453-L489","documentation":"When delivering each location update, the manager calls noteAppOpForEffectiveGranularity to record the corresponding app op (e.g._FINE_LOCATION/_COARSE_LOCATION) for the client. If the app op is denied (user restricted location for the app, e.g. 'Allow only while in use', or app op set to ignore/errored), the update cannot be attributed and a RuntimeException 'app op denied' is thrown.","triggerScenarios":"A location update arrives for a client whose location app op is currently denied in AppOps (user turned off location permission for the app in Settings, or a device-owner/profile-owner restriction denies it) while the request is still registered.","commonSituations":"User switches the app's location permission to 'Deny' or battery-restricted mode without the app removing its listener; work-profile or enterprise policy blocking location; OEM-level app op restrictions.","solutions":["Re-request location permission / ask the user to re-enable location for the app in Settings","Remove and re-register the location request after permission state changes so the app op is re-evaluated","Check permission and app-op state (ContextCompat.checkSelfPermission + AppOpsManager) before and during updates","Fall back to coarse updates or stop requesting updates when the app op is denied"],"exampleFix":"// before\nclient.requestLocationUpdates(request, listener, looper) // never re-checked after denial\n// after\nif (ContextCompat.checkSelfPermission(ctx, ACCESS_FINE_LOCATION) == PERMISSION_GRANTED &&\n    appOpAllowed(AppOpsManager.OPSTR_FINE_LOCATION)) {\n    client.requestLocationUpdates(request, listener, looper)\n}","handlingStrategy":"validation","validationCode":"fun locationAppOpAllowed(ctx: Context): Boolean {\n    val ops = ctx.getSystemService(AppOpsManager::class.java)\n    val mode = ops.noteOpNoThrow(AppOpsManager.OPSTR_FINE_LOCATION, android.os.Process.myUid(), ctx.packageName)\n    return mode == AppOpsManager.MODE_ALLOWED\n}","typeGuard":"fun canReceiveUpdates(ctx: Context) =\n    ContextCompat.checkSelfPermission(ctx, ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED &&\n    locationAppOpAllowed(ctx)","tryCatchPattern":"try {\n    client.requestLocationUpdates(request, listener, looper)\n} catch (e: RuntimeException) {\n    if (e.message == \"app op denied\") {\n        stopLocationUpdates(); showPermissionRationale()\n    } else throw e\n}","preventionTips":["Check both runtime permission and AppOps mode before registering updates","Listen for permission/app-op changes and unregister listeners when denied","Guide users to Settings when location is restricted for the app","Fall back gracefully to coarse or no updates when the op is denied"],"tags":["location","android","appops","permissions"],"backgroundTag":"permission-denied","analyzedSha":"157c9d86ac46c195a86c2f15ab55c84036223f95","analyzedAt":"2026-09-06T17:27:33.892Z","contentChangedAt":"2026-09-06T17:27:33.892Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}