Skip to content

quran/mobile-sync

Repository files navigation

mobile-sync

Kotlin Multiplatform sync/data stack for Quran mobile apps.

This repository contains shared modules used by Android and iOS apps for:

  • Authentication (OIDC)
  • Local persistence (SQLDelight)
  • Sync engine orchestration
  • A unified app-level service API (SyncService)

Table of Contents

Architecture

auth + persistence + syncengine are composed in sync-pipelines, exposed through a DI graph (AppGraph / SharedDependencyGraph), and exported to iOS through umbrella.

flowchart LR
  UI[Android/iOS UI] --> S[SyncService]
  S --> A[Auth Module]
  S --> P[Persistence Module]
  S --> E[SyncEngine Module]
  U[Umbrella XCFramework] --> UI
Loading

Modules

Module Purpose
:auth OIDC login/logout, auth state, token handling
:persistence SQLDelight DB, repositories for bookmarks/collections/notes/reading sessions
:syncengine Core sync engine and scheduling
:sync-pipelines DI graph and SyncService orchestration API
:umbrella iOS XCFramework export (Shared.xcframework)
:demo:android Android sample app (Compose)
:demo:common Shared demo helpers/models
:mutations-definitions Shared mutation/domain definitions

Requirements

  • JDK 17
  • Android Studio (for Android demo)
  • Xcode (for iOS demo)
  • Cocoa toolchain for iOS simulator/device builds

Optional but recommended:

  • local.properties with the OAuth client ID for the Android demo app:
OAUTH_CLIENT_ID=your_client_id

Published library artifacts do not embed credentials. Android apps are OAuth public clients using PKCE and must not embed OAuth client secrets.

Quick Start

  1. Clone and enter the repo:
git clone https://github.com/quran/mobile-sync.git
cd mobile-sync
  1. Ensure Gradle can resolve dependencies:
./gradlew help
  1. Run tests:
./gradlew allTests --stacktrace --continue

Usage

1. Initialize app graph (Android/Kotlin)

Configure the Android app manifest placeholders for the login and post-logout redirect URIs. The :auth artifact contributes com.quran.shared.auth.android.SafeOidcRedirectActivity as the exported browser callback entry point and keeps the upstream OIDC HandleRedirectActivity non-exported.

android {
    defaultConfig {
        manifestPlaceholders["oidcRedirectScheme"] = "com.quran.oauth"
        manifestPlaceholders["oidcRedirectHost"] = "callback"
        manifestPlaceholders["oidcPostLogoutRedirectScheme"] = "com.quran.oauth"
        manifestPlaceholders["oidcPostLogoutRedirectHost"] = "callback"
    }
}

Do not declare or export org.publicvalue.multiplatform.oidc.appsupport.HandleRedirectActivity in the app manifest. If you override AuthConfig.redirectUri or AuthConfig.postLogoutRedirectUri on Android, keep the corresponding manifest placeholders aligned with those URIs.

import com.quran.shared.auth.di.AuthFlowFactoryProvider
import com.quran.shared.persistence.DriverFactory
import com.quran.shared.pipeline.AppEnvironment
import com.quran.shared.pipeline.di.SharedDependencyGraph
import com.quran.shared.pipeline.storage.createMobileSyncStorage
import org.publicvalue.multiplatform.oidc.appsupport.AndroidCodeAuthFlowFactory

val authFactory = AndroidCodeAuthFlowFactory(useWebView = false)
authFactory.registerActivity(activity)
AuthFlowFactoryProvider.initialize(authFactory)

val graph = SharedDependencyGraph.init(
    driverFactory = DriverFactory(context = applicationContext),
    storage = createMobileSyncStorage(applicationContext),
    appEnvironment = AppEnvironment.PRELIVE,
    clientId = appClientId
)

val authService = graph.authService
val syncService = graph.syncService

2. Initialize app graph (iOS/Swift)

import Shared

final class AppContainer {
    static let shared = AppContainer()
    static var graph: AppGraph { shared.graph }

    let graph: AppGraph

    private init() {
        Shared.AuthFlowFactoryProvider.shared.doInitialize()
        let driverFactory = DriverFactory()
        let storage = AppleMobileSyncStorageFactory.shared.create()
        graph = SharedDependencyGraph.shared.doInit(
            driverFactory: driverFactory,
            storage: storage,
            appEnvironment: AppEnvironment.prelive,
            clientId: appClientId,
            clientSecret: appClientSecret
        )
    }
}

Advanced override for custom endpoints remains available:

import com.quran.shared.auth.model.AuthEnvironment
import com.quran.shared.auth.model.AuthConfig
import com.quran.shared.syncengine.SynchronizationEnvironment

val graph = SharedDependencyGraph.init(
    driverFactory = DriverFactory(context = applicationContext),
    storage = createMobileSyncStorage(applicationContext),
    environment = SynchronizationEnvironment(
        endPointURL = "https://custom-sync.example.com/auth"
    ),
    authConfig = AuthConfig(
        environment = AuthEnvironment.PRELIVE,
        clientId = appClientId
    )
)

Android backup exclusions

The shared Android storage uses stable DataStore file names so apps can exclude derived sync state and token-store data from backup or device transfer:

<exclude domain="file" path="datastore/quran_mobile_sync_settings.preferences_pb"/>
<exclude domain="file" path="datastore/org.publicvalue.multiplatform.oidc.tokenstore.preferences_pb"/>

Add those entries to both full-backup-content and Android 12+ data-extraction-rules when app backup is enabled.

3. Use SyncService

Core API examples:

  • Observe: authState, bookmarks, collectionsWithBookmarks, notes
  • Mutations: addBookmark, deleteBookmark, addCollection, deleteCollection, addNote, deleteNote
  • Trigger sync: triggerSync()

Lifecycle note:

  • SyncService is app-scoped. Initialize once via SharedDependencyGraph.init(...).
  • Do not clear app-scoped services from UI/view-model teardown.

4. App environment selection

The published artifact can target either environment at runtime:

  • AppEnvironment.PRELIVE / AppEnvironment.prelive
  • AppEnvironment.PRODUCTION / AppEnvironment.production

AppEnvironment keeps auth and sync aligned by default:

  • PRELIVE -> https://prelive-oauth2.quran.foundation and https://apis-prelive.quran.foundation/auth
  • PRODUCTION -> https://oauth2.quran.foundation and https://apis.quran.foundation/auth

buildkonfig.flavor now only controls the default fallback used when the app does not pass an explicit app environment.

Default fallback (gradle.properties):

buildkonfig.flavor=debug

Override for release-like builds:

./gradlew :auth:build -Pbuildkonfig.flavor=release

Run the Demos

Android demo

  1. Open project in Android Studio.
  2. Run demo/android app module.

CLI build example:

./gradlew :demo:android:assembleDebug

iOS demo

  1. Open: demo/apple/QuranSyncDemo/QuranSyncDemo.xcodeproj
  2. Select QuranSyncDemo scheme.
  3. Run on an iOS Simulator.

The Xcode project includes a Run Script phase that calls:

./gradlew :umbrella:embedAndSignAppleFrameworkForXcode

CLI build example:

xcodebuild -project demo/apple/QuranSyncDemo/QuranSyncDemo.xcodeproj \
  -scheme QuranSyncDemo \
  -configuration Debug \
  -destination 'generic/platform=iOS Simulator' \
  build CODE_SIGNING_ALLOWED=NO

Build, Test, and Release Commands

Build all:

./gradlew build

Run all tests:

./gradlew allTests --stacktrace --continue

Build iOS XCFramework:

./gradlew :umbrella:assembleSharedXCFramework

Build Android demo compile target:

./gradlew :demo:android:compileDebugKotlin

Build iOS KMP target:

./gradlew :sync-pipelines:compileKotlinIosSimulatorArm64

Related Repositories

About

Quran Mobile Sync Engine

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages