LogoSignature Service
Remote Signing Service SDKAndroid SDK

Migration guide to version 3

This guide documents the changes required to migrate from the legacy SDK (old-sdk/android/lib, written in Java, RxJava2-based) to the current SDK (lib, written in Kotlin, coroutines-based).

The public package name is unchanged (com.swisssign.rss.sdk.mobile) and the Maven artifact coordinates are unchanged (ch.sysmosoft.swisssign:swisssign-sdk-android-lib), but almost every public API has changed shape:

AspectOld SDK (old-sdk)New SDK (lib)
LanguageJavaKotlin
Entry pointStatic Rss classRss interface, obtained via buildRss { }
Async modelRxJava2 (Single, Completable)Kotlin coroutines (suspend fun)
ModelsLombok @Builder classesKotlin data class
Error handlingRxJava onError consumerTyped checked exceptions (@Throws) + runCatching
minSdkproject-defined (lower)29

1. Gradle / dependency changes

The artifact coordinates are unchanged:

implementation("ch.sysmosoft.swisssign:swisssign-sdk-android-lib:<version>")

What changed around it:

  • minSdk is now 29. Consumers targeting a lower minSdk must raise it.
  • RxJava2 is no longer a dependency of the SDK. The old SDK exposed io.reactivex.rxjava2:rxjava as an api dependency (i.e. transitively to consumers). The new SDK does not depend on RxJava at all. If your app used the RxJava types only because the SDK required them, you can remove that dependency once migrated.
  • Kotlin coroutines are required. Since the public API now exposes suspend fun, your app module must apply the Kotlin Android plugin and depend on kotlinx-coroutines-android (or at least kotlinx-coroutines-core) to call the SDK conveniently from lifecycleScope / viewModelScope.
  • Lombok / annotation processing is no longer needed for the SDK models — DssProfile and PendingRequest are plain Kotlin data classes now.
  • Internal packages are excluded from the published sources jar. The new build (lib/build.gradle.kts) explicitly excludes **/client/** and **/util/** from the sources artifact. If your app was (incorrectly) depending on internal implementation classes such as com.swisssign.rss.sdk.mobile.client.DefaultRssService or the util package (BiometricUtils, JsonRpcUtils, RssUtils), those are not part of the public contract and must not be relied upon — use the public interfaces (Rss, SessionService, RssService, DssService) instead.

2. Rss — SDK entry point

Old: static initialization

// Initialize
Rss.initialize(context);

// or with legacy Properties-based config (deprecated)
Rss.initialize(context, properties);

// Configure (must be done before initialize)
Rss.configuration().setSecServerUrl(new URL("https://..."));
Rss.configuration().setLicense("1234");

boolean initialized = Rss.isInitialized();
SessionService session = Rss.getSessionService(token);
Rss.release();

New: instance obtained via buildRss { }

The static class was replaced by an Rss interface. You build (and implicitly initialize) an instance via the buildRss builder function:

val rss: Rss = buildRss(context) {
    rssLicense = "1234"                       // Mandatory
    rssSecServerUrl = "https://..."            // Mandatory
    rssWorkflowEngineUrl = "https://..."       // Optional
    rssHttpConfiguration = httpConfig          // Optional
    transmitDeviceSensitiveData = false        // Optional, default false
}

val initialized = rss.isInitialized()
val session = rss.getSessionService(token)
rss.release()

What to change:

  • Replace Rss.initialize(context) / Rss.initialize(context, properties) with a buildRss(context) { ... } call. There is no equivalent for the deprecated Properties-based overload — set rssLicense, rssSecServerUrl, rssWorkflowEngineUrl explicitly on the builder instead.
  • Keep a reference to the returned Rss instance (e.g. inject it as a singleton) — all further calls (configuration(), isInitialized(), getSessionService(), release(), isDeviceCompliant(), getEnrolledUsers(), disenrollUser()) are now instance methods instead of static calls.
  • rss.getSessionService(token) now throws IllegalStateException if the SDK is not initialized (same behavior as before, just documented via KDoc).

isDeviceCompliant — int constants replaced by a sealed type

Old:

int result = Rss.isDeviceCompliant(context);
if (result == Rss.DEVICE_COMPLIANT) {
    // ok
} else if (result == Rss.ERR_NO_HARDWARE) {
    // no hardware
} else if (result == Rss.ERR_BIOMETRIC_NOT_ENROLLED) {
    // no biometrics enrolled
}

New:

when (val result = rss.isDeviceCompliant(context)) {
    is Rss.DeviceCompliance.Compliant -> { /* ok */ }
    is Rss.DeviceCompliance.ErrNoHardware -> { /* no hardware */ }
    is Rss.DeviceCompliance.ErrBiometricNotEnrolled -> { /* no biometrics enrolled */ }
}

What to change: replace the int comparisons with a when over the new Rss.DeviceCompliance sealed interface. This is exhaustive at compile time, so the compiler will flag missing branches if new compliance states are added in the future.

Other static → instance methods

Old (Rss.* static)New (rss.* instance)
Rss.configuration()rss.configuration()
Rss.getEnrolledUsers(context)rss.getEnrolledUsers(context)
Rss.disenrollUser(context, username)rss.disenrollUser(context, username)

3. SessionService

openSession — Single<RssService> → suspend fun

Old:

sessionService.openSession(secret)
    .subscribe(
        rssService -> { /* success */ },
        error -> {
            if (error instanceof AuthenticationException) { /* ... */ }
            // AccountLockedException, UserDisenrolledException, AccessDeniedException,
            // ServerException, IOException
        }
    );

New (recommended pattern — see section 6):

lifecycleScope.launch {
    runCatching { sessionService.openSession(secret) }
        .onSuccess { rssService -> /* success */ }
        .onFailure { error ->
            when (error) {
                is AuthenticationException -> { /* ... */ }
                is UserDisenrolledException -> { /* ... */ }
                is AccountLockedException -> { /* ... */ }
                is AccessDeniedException -> { /* ... */ }
                is ServerException -> { /* ... */ }
                is IOException -> { /* ... */ }
            }
        }
}

Other changes

Old (Java)New (Kotlin)Notes
String getUsername()val userName: Stringnow a property, not a method
boolean isEnrolled()fun isEnrolled(): Booleanunchanged behavior
boolean isSessionValid()fun isSessionValid(): Booleanunchanged behavior
void closeSession()fun closeSession()unchanged behavior
void disenroll()fun disenroll()now declares @Throws(IllegalArgumentException::class) if the user isn't enrolled
RssService getRssService()fun getRssService(): RssServicestill throws IllegalStateException if session isn't valid

What to change: update sessionService.getUsername() call sites to sessionService.userName. Wrap openSession(...) in a coroutine + runCatching instead of .subscribe(...).


4. RssService

Old (Java)New (Kotlin)
Single<FirebaseMessagingOptions> getFirebaseMessagingOptions()suspend fun getFirebaseMessagingOptions(): FirebaseMessagingOptions
Completable registerPushNotificationToken(String token)suspend fun registerPushNotificationToken(token: String)
void addSessionStatusListener(SessionStatusListener listener)fun addSessionStatusListener(listener: SessionStatusListener) (throws InvalidSessionException)
SmartContractService getSmartContractService()fun getSmartContractService(): SmartContractService (throws InvalidSessionException)
DssService getDssService()fun getDssService(): DssService (throws InvalidSessionException)

A new checked exception, InvalidSessionException, is now thrown by these APIs when the session is no longer valid — previously this case was either unspecified or surfaced as a generic IllegalStateException. Add a catch/onFailure branch for it.

Example:

lifecycleScope.launch {
    runCatching { rssService.registerPushNotificationToken(token) }
        .onSuccess { /* registered */ }
        .onFailure { error ->
            when (error) {
                is InvalidSessionException -> { /* session expired, re-authenticate */ }
                is AccessDeniedException -> { /* ... */ }
                is IOException -> { /* ... */ }
            }
        }
}

5. DssService

All RxJava-returning methods became suspend fun. Two APIs also changed shape, and one API is new:

Old (Java)New (Kotlin)Notes
Single<List<DssProfile>> getAvailableProfiles()suspend fun getAvailableProfiles(): List<DssProfile>
(none)suspend fun getAllProfiles(): List<DssProfile>New API — returns all profiles, including ones requiring identification
Single<DssProfile> getProfile(String dssProfileId)suspend fun getProfile(dssProfileId: String): DssProfile?Return type is now nullable — returns null instead of erroring when not found
Single<DssProfile.RegistrationStatus> getRegistrationStatus(String id)suspend fun getRegistrationStatus(dssProfileId: String): DssProfile.RegistrationStatus
Completable activateProfile(FragmentActivity a, String dssProfileId)suspend fun activateProfile(fragmentActivity: FragmentActivity, dssProfileId: String)
Completable disableProfile(String dssProfileId)suspend fun disableProfile(dssProfileId: String)
Single<PendingRequest> getPendingRequest(String dssProfileId, String requestId)suspend fun getPendingRequest(dssProfileId: String, requestId: String): PendingRequest?now nullable, can also throw InvalidSessionException, BiometricException
Single<List<PendingRequest>> getPendingRequests(String dssProfileId)suspend fun getPendingRequests(dssProfileId: String): List<PendingRequest>can also throw InvalidSessionException, AliasNotFoundException, BiometricException
Completable signPendingRequest(FragmentActivity a, PendingRequest req)suspend fun signPendingRequest(fragmentActivity: FragmentActivity, pendingRequest: PendingRequest)
boolean isBiometricIntegrityValid(String dssProfileId)fun isBiometricIntegrityValid(dssProfileId: String): Booleannow declares @Throws(InvalidSessionException::class)

Two new checked exceptions are introduced across DssService:

  • InvalidSessionException — thrown when the session backing the DssService is no longer valid.
  • AliasNotFoundException (ch.sysmosoft.sense.android.core.signature) — thrown by getPendingRequests when the given dssProfileId doesn't have an associated key alias.

DssException and BiometricException (and KeyPermanentlyInvalidatedException for signPendingRequest) are still thrown for the same reasons as before.

Example: fetching and signing a pending request

Old:

dssService.getAvailableProfiles()
    .flatMap(profiles -> dssService.getPendingRequests(profiles.get(0).getId()))
    .subscribe(
        pendingRequests -> {
            dssService.signPendingRequest(fragmentActivity, pendingRequests.get(0))
                .subscribe(
                    () -> { /* signed */ },
                    error -> { /* DssException / BiometricException / IOException / ... */ }
                );
        },
        error -> { /* DssException / IOException */ }
    );

New:

lifecycleScope.launch {
    runCatching {
        val profiles = dssService.getAvailableProfiles()
        val pendingRequests = dssService.getPendingRequests(profiles.first().id)
        dssService.signPendingRequest(fragmentActivity, pendingRequests.first())
    }.onSuccess {
        /* signed */
    }.onFailure { error ->
        when (error) {
            is DssException -> { /* check error.errorCode */ }
            is BiometricException -> { /* check error.code */ }
            is KeyPermanentlyInvalidatedException -> { /* re-activate profile */ }
            is InvalidSessionException -> { /* re-authenticate */ }
            is AliasNotFoundException -> { /* profile not activated on this device */ }
            is IOException -> { /* network/server error */ }
        }
    }
}

Because all three calls are suspend fun, they can be chained directly with regular Kotlin control flow inside a single runCatching block instead of nesting .flatMap()/.subscribe() calls.


Every suspend function in the new SDK declares its checked exceptions via @Throws(...). The recommended way to consume them is not a manual try { } catch { }, but Kotlin's runCatching { }, which returns a Result<T> you can branch on with .onSuccess { } / .onFailure { } (or inspect via result.exceptionOrNull()):

viewModelScope.launch {
    runCatching {
        sessionService.openSession(secret)
    }.onSuccess { rssService ->
        _state.value = SessionState.Open(rssService)
    }.onFailure { error ->
        _state.value = when (error) {
            is AuthenticationException -> SessionState.InvalidSecret
            is UserDisenrolledException -> SessionState.Disenrolled
            is AccountLockedException -> SessionState.Locked
            is IOException -> SessionState.NetworkError
            else -> SessionState.UnknownError(error)
        }
    }
}

Guidelines:

  • Always launch suspend calls from a structured CoroutineScope (lifecycleScope, viewModelScope, or a scope you own and cancel yourself) — never from GlobalScope.
  • Wrap the suspend call(s) in runCatching { } rather than try/catch; it keeps error handling declarative and composable (.map, .recover, .getOrElse, etc.) and avoids accidentally swallowing CancellationException the way a broad catch (e: Exception) would.
  • Use a when over the caught Throwable to branch on the specific documented exception types (see the @Throws annotation on each function for the exhaustive list) instead of a generic catch-all.
  • This replaces the old pattern of passing two lambdas (onSuccess, onError) to .subscribe(...) on a Single/Completable.

7. Models

DssProfile — Lombok builder → data class

Old:

DssProfile profile = DssProfile.builder()
    .id("abc")
    .name("My profile")
    .signatureLevel(DssProfile.SignatureLevel.QES)
    .signatureJurisdiction(DssProfile.SignatureJurisdiction.ZERTES)
    .registrationStatus(DssProfile.RegistrationStatus.REGISTERED)
    .build();

String id = profile.getId();

New:

val profile = DssProfile(
    id = "abc",
    name = "My profile",
    signatureLevel = DssProfile.SignatureLevel.QES,
    signatureJurisdiction = DssProfile.SignatureJurisdiction.ZERTES,
    registrationStatus = DssProfile.RegistrationStatus.REGISTERED,
    identificationStatus = DssProfile.IdentificationStatus.VALID
)

val id = profile.id

What to change:

  • Replace DssProfile.builder()....build() calls with the data class constructor (SDK consumers normally only read profiles returned by DssService, so this mainly affects test code / fakes).
  • Replace profile.getX() accessor calls with profile.x property access.
  • A new field, identificationStatus, was added (enum DssProfile.IdentificationStatus: VALID, IDENTIFICATION_REQUIRED, IDENTIFICATION_RENEWAL_REQUIRED). Any code constructing a DssProfile manually (e.g. test fakes) must now supply this field.

PendingRequest — Lombok builder → data class, deprecated accessors removed

Old:

PendingRequest request = PendingRequest.builder()
    .requestId("r1")
    .dssProfileId("p1")
    .digestIds(digestIds)
    .documentHashes(hashes)
    .build();

String digestId = request.getDigestId();       // deprecated since 2.5.0
byte[] hash = request.getDocumentHash();        // deprecated since 2.5.0
List<String> digestIds = request.getDigestIds();

New:

val request = PendingRequest(
    requestId = "r1",
    dssProfileId = "p1",
    digestIds = digestIds,
    documentHashes = hashes
)

val digestIds = request.digestIds
val hashes = request.documentHashes
val transactionNumber = request.transactionNumber

What to change:

  • The already-deprecated singular accessors getDigestId() and getDocumentHash() (deprecated since 2.5.0) have been removed. Use digestIds.first() / documentHashes.first() if you need a single value.
  • All fields now have default values ("" / empty collections), so you can omit fields you don't need when constructing instances in tests.
  • Property access (request.requestId) replaces getter calls (request.getRequestId()).
  • getTransactionNumber() becomes the transactionNumber property (backed by the same TransactionNumberUtils.deriveTransactionNumber, now computed lazily).

DssException — unchanged API surface

try {
    // ...
} catch (e: DssException) {
    val code = e.errorCode
}

DssException(errorCode: Long) keeps the same constructor and errorCode property as before (previously a getter, getErrorCode()); only the implementation moved from a hand-written Java class to a concise Kotlin class. Java callers can still use e.getErrorCode(); Kotlin callers use e.errorCode.

InvalidSessionException — new

class InvalidSessionException : Exception("No valid session found. Please open a new session")

This new, unchecked-from-Java/checked-from-Kotlin exception is now thrown by RssService and DssService methods (see sections 4 and 5) whenever the underlying session is no longer valid. Add handling for it wherever you previously relied on a generic IllegalStateException for this case.


8. Internal utilities (util package)

BiometricUtils, JsonRpcUtils, and RssUtils were ported to Kotlin as-is, but the new build (lib/build.gradle.kts) explicitly excludes the client and util packages from the published sources jar. These classes were never part of the supported public API of the SDK. If your integration happened to reference them directly, replace those calls with the public interfaces (Rss, SessionService, RssService, DssService) — direct use of internal classes is not supported and may break without notice in future releases.


9. Migration checklist

  • Raise your app's minSdk to 29 if it is lower.
  • Ensure your app module has the Kotlin Android plugin and kotlinx-coroutines-android (or -core) available.
  • Replace Rss.initialize(context[, properties]) / Rss.configuration() / static calls with val rss = buildRss(context) { rssLicense = ...; rssSecServerUrl = ... } and keep a reference to rss for subsequent calls.
  • Replace int comparisons against Rss.DEVICE_COMPLIANT / Rss.ERR_NO_HARDWARE / Rss.ERR_BIOMETRIC_NOT_ENROLLED with a when over Rss.DeviceCompliance.
  • Replace sessionService.getUsername() with sessionService.userName.
  • Replace every .subscribe(onSuccess, onError) call on a Single/Completable returned by the SDK with a coroutine launch + runCatching { }.onSuccess { }.onFailure { } (see section 6).
  • Add when/is branches for the new InvalidSessionException (thrown by RssService and DssService) and AliasNotFoundException (thrown by DssService.getPendingRequests).
  • Update calls to dssService.getProfile(id) and dssService.getPendingRequest(profileId, requestId) to handle a nullable result instead of an error.
  • Replace DssProfile.builder()...build() / PendingRequest.builder()...build() call sites (mainly test fakes) with the Kotlin data class constructors, supplying the new identificationStatus field for DssProfile.
  • Replace any use of the deprecated PendingRequest.getDigestId() / getDocumentHash() with digestIds.first() / documentHashes.first().
  • Remove direct dependencies on internal client/util classes, if any, and use the public interfaces instead.
  • Remove any now-unused direct dependency on io.reactivex.rxjava2:rxjava that was only present because of the old SDK.

10. Appendix — full API mapping

AreaOld (Java)New (Kotlin)
Entry pointRss.initialize(context)buildRss(context) { ... }
Entry pointRss.initialize(context, properties) (deprecated)(removed — configure via builder properties)
Entry pointRss.configuration() (static)rss.configuration()
Entry pointRss.isInitialized() (static)rss.isInitialized()
Entry pointRss.getSessionService(token) (static)rss.getSessionService(token)
Entry pointRss.release() (static)rss.release()
Entry pointRss.isDeviceCompliant(context) → intrss.isDeviceCompliant(context) → Rss.DeviceCompliance
Entry pointRss.getEnrolledUsers(context) (static)rss.getEnrolledUsers(context)
Entry pointRss.disenrollUser(context, username) (static)rss.disenrollUser(context, username)
SessionServiceSingle<RssService> openSession(char[])suspend fun openSession(CharArray?): RssService
SessionServicegetUsername(): Stringval userName: String
SessionServiceisEnrolled(), isSessionValid(), closeSession(), disenroll(), getRssService()unchanged signatures, now Kotlin; disenroll()/getRssService() document @Throws
RssServiceSingle<FirebaseMessagingOptions> getFirebaseMessagingOptions()suspend fun getFirebaseMessagingOptions(): FirebaseMessagingOptions
RssServiceCompletable registerPushNotificationToken(token)suspend fun registerPushNotificationToken(token: String)
RssServiceaddSessionStatusListener, getSmartContractService, getDssServiceunchanged signatures, now throw InvalidSessionException
DssServiceSingle<List<DssProfile>> getAvailableProfiles()suspend fun getAvailableProfiles(): List<DssProfile>
DssService(none)suspend fun getAllProfiles(): List<DssProfile> (new)
DssServiceSingle<DssProfile> getProfile(id)suspend fun getProfile(id): DssProfile? (nullable)
DssServiceSingle<DssProfile.RegistrationStatus> getRegistrationStatus(id)suspend fun getRegistrationStatus(id): DssProfile.RegistrationStatus
DssServiceCompletable activateProfile(activity, id)suspend fun activateProfile(activity, id)
DssServiceCompletable disableProfile(id)suspend fun disableProfile(id)
DssServiceSingle<PendingRequest> getPendingRequest(profileId, requestId)suspend fun getPendingRequest(profileId, requestId): PendingRequest? (nullable)
DssServiceSingle<List<PendingRequest>> getPendingRequests(profileId)suspend fun getPendingRequests(profileId): List<PendingRequest>
DssServiceCompletable signPendingRequest(activity, request)suspend fun signPendingRequest(activity, request)
DssServiceboolean isBiometricIntegrityValid(id)fun isBiometricIntegrityValid(id): Boolean (throws InvalidSessionException)
ExceptionsDssException(errorCode) / getErrorCode()DssException(errorCode) / errorCode (unchanged behavior)
Exceptions(none / generic IllegalStateException)InvalidSessionException (new)
Exceptions(none)AliasNotFoundException used in getPendingRequests (newly surfaced)
ModelDssProfile.builder()....build() + gettersDssProfile(...) data class + properties; new identificationStatus field
ModelPendingRequest.builder()....build() + gettersPendingRequest(...) data class + properties (with defaults)
ModelPendingRequest.getDigestId() (deprecated)(removed — use digestIds.first())
ModelPendingRequest.getDocumentHash() (deprecated)(removed — use documentHashes.first())

On this page