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#
| Operation | Supported 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
HaapiConfigurationin both the host app and the widget receiver. Encoding it in a shared module is the simplest approach. - Shared
SharedPreferencesfor 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
Storageimplementation 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.
How to implement this: OAuthTokenManager · DCR · Android UIWidget · How to Use HAAPI in an App Extension or Widget