diff --git a/docs/guides/building_native_android_solid_apps_with_androidsolidservices.md b/docs/guides/building_native_android_solid_apps_with_androidsolidservices.md new file mode 100644 index 0000000..e5405e1 --- /dev/null +++ b/docs/guides/building_native_android_solid_apps_with_androidsolidservices.md @@ -0,0 +1,481 @@ +# Building Native Android Solid Apps with AndroidSolidServices + +[AndroidSolidServices](https://github.com/pondersource/Android-Solid-Services), referred to here as **SolidAndroidService (SAS)**, is an open-source project that brings Solid support to native Android development. It solves a core problem in the Android ecosystem: without it, every Solid-enabled app has to build its own authentication flow and token management from scratch, and users must log in separately to each app. + +SAS acts as a **centralised Solid identity layer** on the device. Users authenticate once inside the SAS host app; any other app that integrates with SAS can then reuse that session without ever touching the user's credentials. + +## The Two Libraries + +SAS ships two independent libraries, referred to throughout this guide as **SAC** and **SAA**. Pick the one that fits your use-case: + +| | **SolidAndroidClient (SAC)** | **SolidAndroidApi (SAA)** | +|--- |--- |--- | +| **How it works** | Talks to the SAS host app via IPC | Communicates directly with the Solid IDP and pod server | +| **Auth management** | Handled by SAS — your app never sees tokens | Your app manages its own OIDC + DPoP session | +| **Requires SAS app** | Yes | No | +| **Best for** | Most third-party apps | Apps that need full auth control or run without SAS | +| **Artifact** | `com.pondersource.solidandroidclient:solidandroidclient` | `com.pondersource.solidandroidapi:solidandroidapi` | + +## Prerequisites + +- Android Studio with a project targeting **API level 26+** +- **JDK 17** (JBR v17.0.9 is recommended) +- A Solid account and Pod — create a free one at [solidcommunity.net](https://solidcommunity.net) +- *(SAC only)* The [SAS host app](https://github.com/pondersource/Android-Solid-Services/releases) installed on the target device + +## 1. Install the SAS Host App + +If you are using **SAC**, the SAS host app must be installed on the device. Download the latest APK from the [GitHub Releases](https://github.com/pondersource/Android-Solid-Services/releases) page, enable *Install from unknown sources* in device settings, and install it. Launch the app and sign in with your Solid pod credentials. + +If you are using **SAA** only, this step is not required. + +## 2. Add the Dependency + +Open your module-level `build.gradle.kts` and add the library you need: + +=== "SolidAndroidClient" + + ```kotlin + // build.gradle.kts (module level) + android { + defaultConfig { + manifestPlaceholders["appAuthRedirectScheme"] = "YOUR_APP_PACKAGE_NAME" + } + } + + dependencies { + implementation("com.pondersource.solidandroidclient:solidandroidclient:0.3.1") + } + ``` + +=== "SolidAndroidApi" + + ```kotlin + // build.gradle.kts (module level) + android { + defaultConfig { + manifestPlaceholders["appAuthRedirectScheme"] = "YOUR_APP_PACKAGE_NAME" + } + } + + dependencies { + implementation("com.pondersource.solidandroidapi:solidandroidapi:0.3.1") + } + ``` + +Both libraries are published to **Maven Central**, so no additional repository configuration is needed. + +## 3. Authenticate with Solid + +!!! info "Specification basis" + The authentication flow implemented by both libraries is based on two specifications: + + - **[Solid Protocol v0.11.0](https://solidproject.org/TR/2024/protocol-20240512)** (Draft Community Group Report, 12 May 2024) — defines how clients authenticate with and access Solid pods. + - **[Solid OIDC v0.1.0](https://solidproject.org/TR/oidc)** (Community Group Report, 2022-03-28) — specifies the OpenID Connect profile used by Solid, including DPoP-bound access tokens. + + Both specifications are published by the [Solid Community Group](https://www.w3.org/groups/cg/solid/) and are not official W3C Standards. Contents remain subject to change. + +=== "SolidAndroidClient" + + All three SDK clients are singletons returned by the `Solid` companion object. Start by obtaining a `SolidSignInClient` and waiting for the IPC connection to the SAS host app to become ready: + + ```kotlin + import com.pondersource.solidandroidclient.sdk.Solid + import com.pondersource.solidandroidclient.sdk.SolidSignInClient + import kotlinx.coroutines.flow.collectLatest + + class MyViewModel(context: Context) : ViewModel() { + + private val signInClient: SolidSignInClient = Solid.getSignInClient(context) + + fun connectAndLogin() { + viewModelScope.launch { + // Wait until the IPC channel to SAS is established + signInClient.authServiceConnectionState().collectLatest { connected -> + if (connected) { + val account = signInClient.getAccount() + if (account == null) { + // No access grant yet — ask the user to approve + signInClient.requestLogin { granted, error -> + if (granted) { + // Your app now has an authorised session + } else { + // User denied access or an error occurred + } + } + } + // account != null means we already have a grant + } + } + } + } + + fun logout() { + viewModelScope.launch { + signInClient.disconnectFromSolid { success, error -> + // Access grant revoked + } + } + } + } + ``` + + !!! note + Always await a `true` emission from `authServiceConnectionState()` before calling any other client method. The IPC binding to the SAS host app is asynchronous. + +=== "SolidAndroidApi" + + The `Authenticator` class manages an OpenID Connect session with DPoP support. It opens a browser tab for the user to log in, then processes the authorization callback your app receives. + + ```kotlin + import com.pondersource.solidandroidapi.Authenticator + + class MyViewModel(context: Context) : ViewModel() { + + private val authenticator: Authenticator = Authenticator.getInstance(context) + + // Step 1 — launch browser login + fun startLogin(issuerUri: String): Intent { + val intent = authenticator.createAuthenticationIntent( + oidcIssuer = issuerUri, //https://login.inrupt.com for example + appName = YOUR_APP_NAME, + redirectUri = AUTH_APP_REDIRECT_URL //YOUR_APP_PACKAGE_NAME:/oauth2redirect + ) + return intent + //Handle the intent in your ComposeComponent/Activity/Fragment. + } + + // Step 2 — call this from your ComposeComponent/Activity/Fragment after the browser redirects back + fun handleCallback( + authorizationResponse: AuthorizationResponse?, + authorizationException: AuthorizationException? + ) { + viewModelScope.launch { + try { + authenticator.submitAuthorizationResponse( + authorizationResponse, + authorizationException + ) + + } catch (_: Exception) {} + + if(authenticator.isUserAuthorized()) { + //User logged-in + } else { + //Authentication failed + } + } + } + + fun logout(webId: String): Intent? { + val (intent, errorMessage) = authenticator.getTerminationSessionIntent( + webId = webId, + logoutRedirectUrl = YOUR_LOGOUT_REDIRECT_URL + ) + return intent + //intent would be null if failed to make the intent and can read errorMessage. + //Handle the intent in your ComposeComponent/Activity/Fragment. + } + } + ``` + + Once authenticated, you can inspect the active session: + + ```kotlin + // Observe the active user's WebID as a Flow + authenticator.activeWebIdFlow.collect { webId -> + println("Logged in as: $webId") + } + + // Check auth status + authenticator.isAuthorizedFlow.collect { authorised -> + if (authorised) { + // Safe to make pod requests + } + } + ``` + +## 4. Read and Write Pod Resources + +!!! info "Specification basis" + Resource management in Solid is governed by two specifications: + + - **[Linked Data Platform 1.0 (LDP)](https://www.w3.org/TR/ldp/)** (W3C Recommendation, 26 February 2015) — defines the HTTP operations for reading and writing resources on the web: GET, POST, PUT, and DELETE, as well as the container model (`ldp:BasicContainer`, `ldp:contains`) used to organise resources in a pod. + - **[Solid Protocol v0.11.0, §5 — Reading and Writing Resources](https://solidproject.org/TR/2024/protocol-20240512#reading-and-writing-resources)** (Draft Community Group Report, 12 May 2024) — specifies how Solid servers handle resource access, creation, patching, deletion, and the expected HTTP status codes. It is compatible with LDP and extends it with Solid-specific requirements. + +Both libraries expose the same conceptual resource types: + +- **`RDFSource`** — structured Turtle / JSON-LD data +- **`NonRDFSource`** — raw files (images, binaries, plain text) +- **`SolidContainer`** — LDP container (directory) + +=== "SolidAndroidClient" + + Obtain a `SolidResourceClient` from the `Solid` companion object. All operations are `suspend` functions and throw subclasses of `SolidException` on failure. + + ```kotlin + import com.pondersource.solidandroidclient.sdk.Solid + import com.pondersource.solidandroidclient.sdk.SolidResourceClient + import com.pondersource.solidandroidclient.sdk.Exceptions.SolidException + + val resourceClient: SolidResourceClient = Solid.getResourceClient(context) + + viewModelScope.launch { + try { + resourceClient.resourceServiceConnectionState().collect { servicehasConnected -> + if(servicehasConnected) { + // Fetch the authenticated user's WebID document + val webId = resourceClient.getWebId() + + // Read a resource from the pod + val note = resourceClient.read( + "https://yourpod.example/notes/note1.ttl", + MyNote::class.java + ) + + // Create a new resource on the pod + val newNote = MyNote(body = "Hello Solid!") + resourceClient.create(newNote) + + // Update an existing resource + note.body = "Updated content" + resourceClient.update(note) + + // Delete a resource + resourceClient.delete(note) + } + } + + } catch (e: SolidException) { + // Handle specific subclasses as needed: + // SolidAppNotFoundException, SolidNotLoggedInException, + // SolidResourceException (NotPermissionException, etc.) + Log.e("Solid", "Operation failed", e) + } + } + ``` + + Your data classes must extend `RDFSource` or `NonRDFSource` from the shared module: + + ```kotlin + import com.pondersource.shared.RDFSource + + class MyNote : RDFSource { + private val bodyKey = rdf.createIRI("body") + var body: String = "" + + constructor( + identifier: URI, + mediaType: MediaType? = null, + dataset: RdfDataset? = null, + headers: Headers? = null + ): super(identifier, mediaType ?: MediaType.JSON_LD, dataset, headers) + } + ``` + + !!! note + Always await a `true` emission from `resourceServiceConnectionState()` before calling any other client method. The IPC binding to the SAS host app is asynchronous. + +=== "SolidAndroidApi" + + Obtain a `SolidResourceManager` instance. Every operation returns a `SolidNetworkResponse` — a sealed class with `Success`, `Error`, and `Exception` variants, so you never deal with raw exceptions for network calls. + + ```kotlin + import com.pondersource.solidandroidapi.SolidResourceManager + import com.pondersource.shared.SolidNetworkResponse + import java.net.URI + + val profile = authenticator.getActiveProfile() //or call getAllLoggedInProfiles() to get a list of loggedin profiles and use the one you need. + val resourceManager: SolidResourceManager = SolidResourceManager.getInstance(context, profile) + + viewModelScope.launch { + // Read a resource + when (val response = resourceManager.read( + resource = URI("https://yourpod.example/notes/note1.ttl"), + clazz = MyNote::class.java + )) { + is SolidNetworkResponse.Success -> display(response.data) + is SolidNetworkResponse.Error -> showError(response.errorCode, response.errorMessage) + is SolidNetworkResponse.Exception -> handleException(response.exception) + } + + // Create a resource + val newNote = MyNote(body = "Hello Solid!") + when (val response = resourceManager.create(resource = newNote)) { + is SolidNetworkResponse.Success -> { /* created */ } + is SolidNetworkResponse.Error -> showError(response.errorCode, response.errorMessage) + is SolidNetworkResponse.Exception -> handleException(response.exception) + } + + // Update a resource + newNote.body = "Updated content" + resourceManager.update(newResource = newNote) + + // Delete a resource + resourceManager.delete(resource = newNote) + + // Delete an entire container recursively + resourceManager.deleteContainer( + containerUri = URI("https://yourpod.example/notes/") + ) + } + ``` + +## 5. Work with Contacts + +!!! warning "Opinionated data model — read before using in production" + Data modules in SAS implement a specific data model. Before building on this, you should understand what is grounded in a formal specification versus what reflects current implementors' practice: + + **Vocabulary-based (W3C standard + Solid extensions):** + + - Core contact and group properties (`vcard:Individual`, `vcard:Group`, `vcard:fn`, `vcard:hasEmail`, `vcard:hasTelephone`, `vcard:hasMember`) come from the [vCard Ontology for RDF](https://www.w3.org/TR/vcard-rdf/) (`http://www.w3.org/2006/vcard/ns#`), a W3C Interest Group Note (22 May 2014). + - The address book structure (`vcard:AddressBook`, `vcard:nameEmailIndex`, `vcard:groupIndex`, `vcard:includesGroup`, `vcard:inAddressBook`, `vcard:WebID`) is defined in the [solid/contacts vCard extension](https://github.com/solid/contacts/blob/main/vcard-extension-addressbook.md) — a Solid Project document that formally specifies the extensions to the W3C vCard ontology used across the Solid ecosystem. These terms are not part of the W3C vCard spec itself. + + **Implementors' knowledge / community conventions (not in Solid TR):** + + - There is currently no published Solid Community Group specification for contacts or address books in the [Solid Technical Reports](https://solidproject.org/TR/). + - The [PDS Interop addressbook conventions](https://pdsinterop.org/conventions/addressbook/) document how solidOS implements address books on pods using these terms, and serve as a practical reference. + - The design of this data module was also informed by [solid-contrib/data-modules](https://github.com/solid-contrib/data-modules), an active NLnet-funded community effort to standardise how Solid apps read and write common data types, including contacts. + - The privacy flags (`isPrivate`), multi-account patterns, and grouping behaviour reflect the design choices of the SAS project specifically and may not be interoperable with all other Solid applications. + + **Ecosystem note:** There are currently divergent approaches to data interoperability in the Solid ecosystem. The [Solid Application Interoperability](https://solidproject.org/TR/sai) specification is one initiative working toward alignment. If broad interoperability is a priority for your application, evaluate these approaches before committing to a data module implementation. + +Both libraries provide a Solid Contacts data module that manages address books, contacts, and groups on the pod. + +=== "SolidAndroidClient" + + ```kotlin + import com.pondersource.solidandroidclient.sdk.Solid + import com.pondersource.solidandroidclient.sdk.SolidContactsDataModule + + val contactsModule: SolidContactsDataModule = Solid.getContactsDataModule(context) + + viewModelScope.launch { + contactDataModule.contactsDataModuleServiceConnectionState().collect{ hasServiceConnected -> + if(hasServiceConnected) { + // List all address books + val books = contactsModule.getAddressBooks() + + // Create a new address book + contactsModule.createAddressBook( + title = "Work Contacts", + isPrivate = false, + storage = "https://yourpod.example/", + ownerWebId = "https://yourpod.example/profile/card#me", + container = "https://yourpod.example/contacts/" + ) + + // Add a contact to an address book + val newContact = NewContact( + name = "Alice Smith", + email = "alicesmith@gmail.com", + phoneNumber = "+1 555 0000", + ) + contactsModule.createNewContact( + addressBookUri = books.first().uri, + newContact = newContact, + groupUris = emptyList() + ) + + // Fetch a contact + val contact = contactsModule.getContact(contactUri = newContact.uri) + + // Add a phone number + contactsModule.addNewPhoneNumber(contactUri = contact.uri, newPhoneNumber = "+1 555 0100") + + // Remove a contact + contactsModule.deleteContact( + addressBookUri = books.privateAddressBookUris.first(), + contactUri = contact.uri + ) + } + } + } + ``` + + !!! note + Always await a `true` emission from `contactsDataModuleServiceConnectionState()` before calling any other client method. The IPC binding to the SAS host app is asynchronous. + + +=== "SolidAndroidApi" + + ```kotlin + import com.pondersource.solidandroidapi.datamodule.SolidContactsDataModule + + val contactsModule: SolidContactsDataModule = SolidContactsDataModule.getInstance(context) + val activeWebId = authenticator.getActiveWebId() + + viewModelScope.launch { + // List all address books + val addressBooks = contactsModule.getAddressBooks(ownerWebId = activeWebId) + + // Create a new address book + contactsModule.createAddressBook( + ownerWebId = activeWebId, + title = "Work Contacts", + isPrivate = false, + storage = YOUR_POD_STORAGE + ) + + // Add a contact + val newContact = NewContact(name = "Alice Smith") + contactsModule.createNewContact( + webid = activeWebId, + addressBookUri = books.privateAddressBookUris.first(), + newContact = newContact, + groupUris = emptyList() + ) + + // Fetch a contact + val contact = contactsModule.getContact(ownerWebId = activeWebId, uri = newContact.uri) + + // Manage groups + val newGroup = contactsModule.createNewGroup( + ownerWebId = activeWebId, + addressBookUri = books.privateAddressBookUris.first(), + title = "Colleagues", + contactUris = listOf(contact.uri) + ) + + // Delete a contact + contactsModule.deleteContact( + ownerWebId = activeWebId, + addressBookUri = books.privateAddressBookUris.first(), + groupString = newGroup.uri + ) + } + ``` + +## Exception Handling (SAC) + +The SAC library uses a structured exception hierarchy so you can handle failure cases precisely: + +```kotlin +import com.pondersource.solidandroidclient.sdk.Exceptions.* + +try { + //Authentication or ResourceManagment action +} catch (e: SolidAppNotFoundException) { + // The SAS host app is not installed — prompt the user to install it +} catch (e: SolidServiceConnectionException) { + // IPC binding failed — retry or check that SAS is running +} catch (e: SolidNotLoggedInException) { + // No authenticated session — redirect to login +} catch (e: NotPermissionException) { + // The pod server rejected the request (403) — check ACL settings +} catch (e: SolidException) { + // Catch-all for any other Solid error +} +``` + +## Further Reading + +- [AndroidSolidServices documentation](https://androidsolidservices.erfangholami.com) +- [API Library reference](https://androidsolidservices.erfangholami.com/api-library/) +- [Client Library reference](https://androidsolidservices.erfangholami.com/client-library/) +- [GitHub repository](https://github.com/pondersource/Android-Solid-Services) +- [Solid Contacts — reference app using SAC](https://github.com/pondersource/Solid-Contacts) +- [Solid Technical Reports](https://solidproject.org/TR/) +- [Linked Data Platform 1.0](https://www.w3.org/TR/ldp/) (W3C Recommendation, 2015) — HTTP operations on resources and containers +- [PDS Interop addressbook conventions](https://pdsinterop.org/conventions/addressbook/) — de-facto solidOS address book structure +- [solid/contacts vCard extension](https://github.com/solid/contacts/blob/main/vcard-extension-addressbook.md) — Solid Project document defining `vcard:AddressBook` and related structural terms diff --git a/docs/index.md b/docs/index.md index 37ff626..69afa2f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,3 +9,4 @@ Welcome! This site provides guides and resources to help you start building appl - [Demo Application using Solid + Next.js + LDO](guides/solid_nextjs_ldo_demo_application) - [Hosting the Community Solid Server in an Azure App Service](guides/hosting_the_community_solid_server_in_an_azure_app_service) - [Demo Application using Solid + Next.js + RDF/JS Wrapper](guides/solid_nextjs_wrapper_demo_application) +- [Building native Android Solid Apps with AndroidSolidServices](guides/building_native_android_solid_apps_with_androidsolidservices) diff --git a/mkdocs.yml b/mkdocs.yml index 7d9e414..6005a3c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -60,6 +60,8 @@ markdown_extensions: - pymdownx.highlight - pymdownx.superfences - pymdownx.smartsymbols + - pymdownx.tabbed: + alternate_style: true - pymdownx.superfences: custom_fences: # Need to fork the theme to make changes https://github.com/squidfunk/mkdocs-material/issues/3665#issuecomment-1060019924