microg/GmsCore · error · RuntimeException

max updates reached

Error message

max updates reached

What it means

DeviceOrientationManager's processNewDeviceOrientation throws RuntimeException("max updates reached") once the number of delivered updates has consumed request.numUpdates (updatesPending <= 0). This enforces the one-shot/limited-update semantics of the orientation request.

Source

Thrown at play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/DeviceOrientationManager.kt:341

            private val request: DeviceOrientationRequest,
            private val listener: IDeviceOrientationListener,
        ) {
            private var updates = 0
            private var lastOrientation: DeviceOrientation? = null

            val updatesPending: Int
                get() = request.numUpdates - updates
            val timePendingMillis: Long
                get() = request.expirationTime - SystemClock.elapsedRealtime()
            val workSource = WorkSource().also { WorkSourceUtil.add(it, clientIdentity.uid, clientIdentity.packageName) }

            fun processNewDeviceOrientation(deviceOrientation: DeviceOrientation) {
                if (timePendingMillis < 0) throw RuntimeException("duration limit reached (expired at ${request.expirationTime}, now is ${SystemClock.elapsedRealtime()})")
                if (lastOrientation != null && abs(lastOrientation!!.headingDegrees - deviceOrientation.headingDegrees) < Math.toDegrees(request.smallestAngleChangeRadians.toDouble())) return
                if (lastOrientation == deviceOrientation) return
                listener.onDeviceOrientationChanged(deviceOrientation)
                if (request.numUpdates != Int.MAX_VALUE) updates++
                if (updatesPending <= 0) throw RuntimeException("max updates reached")
            }
        }
    }
}

View on GitHub (pinned to 157c9d86ac)

Solutions

  1. Unregister the listener after receiving numUpdates callbacks
  2. Set numUpdates = Int.MAX_VALUE if continuous updates are desired
  3. Catch the RuntimeException and clean up/re-register as needed
  4. Ensure only one request per client increments the update counter (avoid duplicate registrations)

Example fix

// before
manager.addDeviceOrientationListener(request, executor, callback) // numUpdates = 1, never removed
// after
val callback = object : DeviceOrientationCallback {
    override fun onDeviceOrientationChanged(o: DeviceOrientation) {
        handle(o)
        if (++received >= request.numUpdates) manager.removeDeviceOrientationListener(this)
    }
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (receivedUpdates >= request.numUpdates) {
    manager.removeDeviceOrientationListener(listener)
}

Type guard

fun DeviceOrientationRequest.isUpdateQuotaExhausted(received: Int): Boolean =
    numUpdates != Int.MAX_VALUE && received >= numUpdates

Try / catch

try {
    handleOrientation(orientation)
} catch (e: RuntimeException) {
    if (e.message == "max updates reached") unregisterAndCleanup()
    else throw e
}

Prevention

When it happens

Trigger: A DeviceOrientationRequest specifying a finite numUpdates receives orientation changes beyond that count — the client keeps receiving events after the update quota is exhausted.

Common situations: Requests created with small numUpdates (e.g. 1) while the sensor keeps reporting; callers not unregistering after receiving all updates; duplicated sensor registrations each incrementing the shared counter.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of microg/GmsCore@157c9d86ac (2026-09-06). Data as JSON: /api/errors/4cbf502fcb49a94b. Report an issue: GitHub.