Android Solid Services Client Library¶
The Android Solid Services Client library lets third-party Android apps plug into the Solid ecosystem by talking to the Android Solid Services host app over IPC. Your app never handles tokens or pod credentials — ASS does that for you.
Use this when:
- You're building an app that works alongside the Android Solid Services host app
- You want the user's Solid session managed centrally (single sign-in)
Otherwise, see the Android Solid Services API if you need direct Solid server access.
Installation¶
// build.gradle.kts (module level)
android {
defaultConfig {
manifestPlaceholders["appAuthRedirectScheme"] = "YOUR_APP_PACKAGE_NAME"
}
}
dependencies {
implementation("com.erfangholami.androidsolidservices:client:0.5.0")
}
Entry Point: Solid¶
All clients are obtained from the Solid companion object. Each is a process-scoped singleton.
val signInClient = Solid.getSignInClient(context)
val resourceClient = Solid.getResourceClient(context)
val contactsModule = Solid.getContactsDataModule(context)
val sharingClient = Solid.getSharingClient(context) // since 0.5.0
val notificationsClient = Solid.getNotificationsClient(context) // since 0.5.0
SolidSignInClient¶
Manages authorization between your app and ASS. Obtain via Solid.getSignInClient(context).
Connection state¶
All IPC services expose a Flow<Boolean> that emits true once the bound service connects and false if it drops. Always wait for true before calling any methods.
signInClient.authServiceConnectionState().collect { connected ->
if (connected) {
// safe to call sign-in methods
}
}
Methods¶
// Emits connection state of the auth IPC service
fun authServiceConnectionState(): Flow<Boolean>
// Returns a SolidSignInAccount if this app is already authorized, null if not.
// Throws SolidException if ASS is not installed, not connected, or no user is logged in.
fun getAccount(): SolidSignInAccount?
// Prompts the user (inside ASS) to grant or deny this app access.
// callBack: (true, null) = granted | (false, null) = denied | (null, error) = error
fun requestLogin(callBack: (Boolean?, SolidException?) -> Unit)
// Revokes this app's access grant.
fun disconnectFromSolid(callBack: (Boolean) -> Unit)
Full auth example¶
val signInClient = Solid.getSignInClient(context)
signInClient.authServiceConnectionState().collect { connected ->
if (connected) {
val account = signInClient.getAccount()
if (account == null) {
signInClient.requestLogin { granted, error ->
when {
error != null -> showError(error)
granted == true -> proceedToApp()
else -> showDeniedMessage()
}
}
} else {
proceedToApp()
}
}
}
SolidResourceClient¶
Reads, creates, updates, and deletes resources on the user's Solid pod via IPC. Obtain via Solid.getResourceClient(context).
All methods are suspend functions. They throw a subclass of SolidResourceException on failure.
Multi-account
Since v0.4.0, methods that operate on pod resources pass through the WebID of the target
account. Save the WebID from getAccount() after login and pass it to each call.
// Emits connection state of the resource IPC service
fun resourceServiceConnectionState(): Flow<Boolean>
// Fetch a user's WebID document
suspend fun getWebId(webId: String): WebId
// Fetch HTTP headers only (no body) — returns SolidMetadata with ETag, Content-Type,
// Content-Length, WAC-Allow, Link relations (acl, describedby, type, storageDescription),
// Accept-Patch/Post/Put, and Last-Modified — with no body transfer.
suspend fun head(webId: String, resourceUrl: String): SolidMetadata
// Read a resource. clazz must extend RDFResource or NonRDFResource.
suspend fun <T : SolidResource> read(webId: String, resourceUrl: String, clazz: Class<T>): T
// Create a new resource on the pod
suspend fun <T : SolidResource> create(webId: String, resource: T): T
// Replace an existing resource. Pass ifMatch (an ETag from a prior read/head) for a
// conditional PUT that fails with 412 on concurrent modification. (ifMatch since 0.5.0)
suspend fun <T : SolidResource> update(webId: String, resource: T, ifMatch: String? = null): T
// Apply an N3 Patch to an RDF resource — atomic partial update, no full read needed.
// Preferred over update() when only a subset of triples changes. (since 0.5.0)
suspend fun patch(webId: String, uri: URI, patch: N3Patch)
// Delete a resource
suspend fun <T : SolidResource> delete(webId: String, resource: T): T
// Read an LDP container; each contained resource is enriched with its own HEAD metadata
suspend fun readContainer(webId: String, containerUrl: String): SolidContainer
// Recursively delete a container and all of its contents (containerUri must end with '/')
suspend fun deleteContainer(webId: String, containerUri: URI)
N3 Patch & ETags over IPC (0.5.0)
head(), patch(), and the ifMatch parameter on update() were added to the in-process
api in 0.4.0; since 0.5.0 they are reachable over IPC too, so client apps get HEAD metadata,
atomic partial RDF updates, and optimistic-concurrency writes without the host app.
Resource types¶
| Base class | Use for |
|---|---|
RDFSource |
Structured RDF data (Turtle, JSON-LD, etc.) |
NonRDFSource |
Raw files — images, text, binary |
Example¶
val resourceClient = Solid.getResourceClient(context)
val webId = signInClient.getAccount()?.webId ?: return // persisted after login
resourceClient.resourceServiceConnectionState().collect { connected ->
if (connected) {
try {
val note = resourceClient.read(
webId,
"https://yourpod.example/notes/hello.ttl",
MyNote::class.java
)
val updated = resourceClient.update(webId, note.copy(body = "updated text"))
} catch (e: SolidException) {
handleException(e)
}
}
}
SolidContactsDataModule¶
Manages address books, contacts, and groups on the pod via IPC. Obtain via Solid.getContactsDataModule(context).
All methods are suspend functions.
// Emits connection state of the contacts IPC service
fun contactsDataModuleServiceConnectionState(): Flow<Boolean>
Address Books¶
suspend fun getAddressBooks(): AddressBookList?
suspend fun createAddressBook(
title: String,
isPrivate: Boolean = true,
storage: String? = null,
ownerWebId: String? = null,
container: String? = null,
): AddressBook?
suspend fun getAddressBook(uri: String): AddressBook?
suspend fun deleteAddressBook(addressBookUri: String): AddressBook?
Contacts¶
suspend fun createNewContact(
addressBookUri: String,
newContact: NewContact,
groupUris: List<String> = emptyList(),
): FullContact?
suspend fun getContact(contactUri: String): FullContact?
suspend fun renameContact(contactUri: String, newName: String): FullContact?
suspend fun addNewPhoneNumber(contactUri: String, newPhoneNumber: String): FullContact?
suspend fun addNewEmailAddress(contactUri: String, newEmailAddress: String): FullContact?
suspend fun removePhoneNumber(contactUri: String, phoneNumber: String): FullContact?
suspend fun removeEmailAddress(contactUri: String, emailAddress: String): FullContact?
suspend fun deleteContact(addressBookUri: String, contactUri: String): FullContact?
Groups¶
suspend fun createNewGroup(
addressBookUri: String,
title: String,
contactUris: List<String> = emptyList(),
): FullGroup?
suspend fun getGroup(groupUri: String): FullGroup?
suspend fun deleteGroup(addressBookUri: String, groupUri: String): FullGroup?
suspend fun addContactToGroup(contactUri: String, groupUri: String): FullGroup?
suspend fun removeContactFromGroup(contactUri: String, groupUri: String): FullGroup?
SolidSharingClient¶
New in 0.5.0
Creates, lists, and revokes shares of pod resources; tracks shares received from others; browses an
owner's catalog; and accepts or rejects access requests. Sharing is enforced by the pod's access
control — Web Access Control (WAC), or Access Control Policy (ACP) on servers that use it. Obtain via
Solid.getSharingClient(context).
All methods are suspend functions and throw a subclass of SolidException on failure. Collect
connectionState() and wait for true before issuing calls.
Share mode and receiver¶
// Access level granted to a receiver
enum class ShareMode { READ, APPEND, WRITE } // surfaced as View / Add / Edit
// Who the share is for
sealed class ShareReceiver {
data class WebIdReceiver(val webId: String) // a single Solid user
data class GroupReceiver(val groupUri: String) // a vcard:Group; members inherit access
data object Public // any agent (resource reachable by URL)
}
A grant writes the full set of implied WAC modes — Add = Read + Append, Edit = Read + Write — and the index collapses them back to one logical mode per receiver.
Methods¶
// Connection state of the sharing IPC service
fun connectionState(): Flow<Boolean>
// --- Shares you give ---
// Grant `receiver` access of `mode` on `resourceUri` (a container shares to its members too).
// When notifyReceiver is true and receiver is a WebID, an offer is delivered to their inbox.
suspend fun createShare(
webId: String,
resourceUri: String,
mode: ShareMode,
receiver: ShareReceiver,
notifyReceiver: Boolean = true,
): GivenShare?
// Change the access mode of an existing share
suspend fun updateShare(webId: String, resourceUri: String, mode: ShareMode, receiver: ShareReceiver): GivenShare?
// Remove a receiver's access (sends a best-effort withdrawal notification for a WebID receiver)
suspend fun revokeShare(webId: String, resourceUri: String, receiver: ShareReceiver)
// Fast read from the on-pod index; re-validate against live ACLs with refreshGivenShares
suspend fun getStoredGivenShares(webId: String): List<GivenShare>
suspend fun refreshGivenShares(webId: String): List<GivenShare>
// Authoritative: read the shares straight from a resource's ACL
suspend fun getGivenSharesForResource(webId: String, resourceUri: String): List<GivenShare>
// Rebuild the index by walking the pod's ACLs (expensive — expose as an explicit user action)
suspend fun rebuildGivenIndex(webId: String): List<GivenShare>
// --- Shares you receive ---
suspend fun getStoredReceivedShares(webId: String): List<ReceivedShare>
suspend fun refreshReceivedShares(webId: String): List<ReceivedShare>
// Start tracking a resource shared with you (e.g. after scanning a QR / opening a share link)
suspend fun addReceivedShare(webId: String, resourceUri: String): ReceivedShare?
suspend fun removeReceivedShare(webId: String, resourceUri: String, ownerWebId: String)
// --- Access grants, requests, catalog ---
// Every observable access relationship (given, received, incoming requests, SAI grants), unified
suspend fun getAccessGrants(webId: String): List<AccessGrant>
// Decide on an incoming access request (from SolidNotificationsClient.listRequests)
suspend fun acceptShareRequest(webId: String, request: ShareRequest): GivenShare?
suspend fun rejectShareRequest(webId: String, request: ShareRequest, reason: String? = null)
// Public catalog of resources others may request access to (the resources stay private)
suspend fun publishCatalogEntry(webId: String, entry: CatalogEntry)
suspend fun removeCatalogEntry(webId: String, resourceUri: String)
suspend fun getOwnerCatalog(viewerWebId: String, ownerWebId: String): List<CatalogEntry>
Example¶
val sharingClient = Solid.getSharingClient(context)
sharingClient.connectionState().collect { connected ->
if (connected) {
try {
// Share a note with Bob at Edit (read + write) and notify his inbox
sharingClient.createShare(
webId = myWebId,
resourceUri = "https://mypod.example/notes/hello.ttl",
mode = ShareMode.WRITE,
receiver = ShareReceiver.WebIdReceiver("https://bob.example/profile/card#me"),
)
// List what I've shared, and act on incoming requests
val given = sharingClient.getStoredGivenShares(myWebId)
val grants = sharingClient.getAccessGrants(myWebId)
} catch (e: SolidException) {
handleException(e)
}
}
}
SolidNotificationsClient¶
New in 0.5.0
Reads and sends share-notification inbox messages over Linked Data Notifications
(LDN). Obtain via Solid.getNotificationsClient(context).
This layer is pull-only — there is no push subscription. Call listNotifications() /
listRequests() from a periodic background worker (Android's effective minimum is 15 minutes) and on
user-initiated refresh. All methods are suspend functions and throw SolidException on failure.
// Connection state of the notifications IPC service
fun connectionState(): Flow<Boolean>
// Incoming share notifications (offers, accepts, withdrawals, rejections)
suspend fun listNotifications(webId: String): List<ShareNotification>
// Incoming access requests — others asking for access to this user's resources
suspend fun listRequests(webId: String): List<ShareRequest>
// Tell receiverWebId you granted / withdrew access (used internally by createShare/revokeShare)
suspend fun sendOffer(ownerWebId: String, receiverWebId: String, resourceUri: String, mode: ShareMode)
suspend fun sendUndo(ownerWebId: String, receiverWebId: String, resourceUri: String)
// Ask ownerWebId for access; decline a request
suspend fun sendRequest(requesterWebId: String, ownerWebId: String, resourceUri: String, requestedMode: ShareMode, summary: String? = null)
suspend fun sendReject(ownerWebId: String, requesterWebId: String, resourceUri: String, reason: String? = null)
// Garbage-collect the inbox: drop Offer/Undo pairs and (optionally) items older than an ISO instant
suspend fun compactInbox(webId: String, olderThanIso: String? = null)
To accept or reject a request returned by listRequests(), pass it to
SolidSharingClient.acceptShareRequest() / rejectShareRequest() — accepting creates the share and
notifies the requester.
Exception Hierarchy¶
All client methods throw a subclass of SolidException:
SolidException
├── SolidAppNotFoundException — ASS app is not installed
├── SolidServiceConnectionException — IPC connection to ASS failed
├── SolidNotLoggedInException — No user is logged in inside ASS
├── SolidServicesDrawPermissionDeniedException — ASS lacks overlay draw permission
└── SolidResourceException
├── NotSupportedClassException — resource class doesn't extend RDFSource/NonRDFSource
├── NotPermissionException — app not authorized to access this resource
├── NullWebIdException — no WebID available for the request
└── UnknownException — unexpected server or protocol error
Example App¶
Solid Contacts is a full open-source app built on this library, demonstrating multi-account contacts management on a Solid pod.