App Widget Support (Android Only)#

Platform-only: Android. App Widgets (home-screen widgets) cannot run the full HAAPI flow.

Android App Widgets have a restricted execution environment — short-lived processes, limited UI APIs, no ability to launch full activities for the HAAPI flow. The framework follows the same pattern as iOS App Extensions: OAuth-only operations are supported (refresh, revoke, use), but acquiring a fresh token via a full HAAPI flow is not. The host app runs the flow and persists the token; the widget reads and refreshes it.

This page documents the working pattern. For the host-app integration, see Android UIWidget .

What the Widget Can and Can’t Do#

OperationSupported in App Widget?
Run a full HAAPI flow (HaapiManager.start(...))❌ Requires full activity
Acquire a fresh access token without an existing refresh token❌ Same root cause
Refresh an access token via OAuthTokenManager
Revoke a refresh token via OAuthTokenManager
Use the access token in authenticated HTTP calls

The host app runs the flow on first launch and persists the token; the widget reads, refreshes, and uses what’s already there.

Prerequisites#

  • The same HaapiConfiguration in both the host app and the widget receiver. Encoding it in a shared module is the simplest approach.
  • Shared SharedPreferences for the OAuth tokens — getSharedPreferences("SharedPreferences", Context.MODE_PRIVATE) is automatically scoped to the app’s package, so both the main process and the widget process see the same data.
  • For DCR fallback: a Storage implementation that both processes can read (typically backed by EncryptedSharedPreferences).

Sharing the Token from Host to Widget#

In the host app, after the flow completes, persist the access and refresh tokens to SharedPreferences:

private val appSharedPreferences: SharedPreferences =
    getSharedPreferences("SharedPreferences", Context.MODE_PRIVATE)

private const val ACCESS_TOKEN = "ACCESS_TOKEN"
private const val REFRESH_TOKEN = "REFRESH_TOKEN"

fun saveOauthModelToken(token: SuccessfulTokenResponse) {
    appSharedPreferences.edit(commit = true) {
        putString(ACCESS_TOKEN, token.accessToken)
        putString(REFRESH_TOKEN, token.refreshToken)
    }
}

In the widget, read them back and pass to OAuthTokenManager for refresh / use.

Refreshing the Token in the Widget#

Use HaapiAccessorFactory(...).createForOAuth() — this skips the parts of the accessor that require a full HAAPI flow and exposes only OAuthTokenManager:

fun refreshFromWidget() {
    val refreshToken = appSharedPreferences.getString(REFRESH_TOKEN, null)
        ?: return  // Host app hasn't authenticated yet

    GlobalScope.launch(Dispatchers.IO) {
        val accessor = HaapiAccessorFactory(haapiConfiguration)
            .createForOAuth()

        val tokenResponse = accessor.oAuthTokenManager.refreshAccessToken(
            refreshToken = refreshToken,
            onCoroutineContext = this.coroutineContext
        )

        when (tokenResponse) {
            is SuccessfulTokenResponse -> {
                saveOauthModelToken(tokenResponse)
                // Use tokenResponse.accessToken to fetch widget data
            }
            is ErrorTokenResponse -> {
                // On invalid_grant: clear stored tokens and prompt user
                // to re-authenticate from the host app on next launch
            }
        }
    }
}

GlobalScope.launch(Dispatchers.IO) keeps the work off the main thread; the widget process can be torn down quickly, so don’t expect long-running coroutines to complete reliably.

App Widgets run in short-lived processes the system can kill at any time. A token refresh that needs a network round trip is well within budget; running custom retry loops or large background jobs isn’t. If refreshAccessToken returns a retryable error, propagate it — let the host app retry the next time the user opens it, rather than blocking the widget UI on a retry the system might cancel.

Was this helpful?