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:
| Aspect | Old SDK (old-sdk) | New SDK (lib) |
|---|---|---|
| Language | Java | Kotlin |
| Entry point | Static Rss class | Rss interface, obtained via buildRss { } |
| Async model | RxJava2 (Single, Completable) | Kotlin coroutines (suspend fun) |
| Models | Lombok @Builder classes | Kotlin data class |
| Error handling | RxJava onError consumer | Typed checked exceptions (@Throws) + runCatching |
minSdk | project-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:
minSdkis now29. Consumers targeting a lowerminSdkmust raise it.- RxJava2 is no longer a dependency of the SDK. The old SDK exposed
io.reactivex.rxjava2:rxjavaas anapidependency (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 onkotlinx-coroutines-android(or at leastkotlinx-coroutines-core) to call the SDK conveniently fromlifecycleScope/viewModelScope. - Lombok / annotation processing is no longer needed for the SDK models —
DssProfileandPendingRequestare plain Kotlindata 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 ascom.swisssign.rss.sdk.mobile.client.DefaultRssServiceor theutilpackage (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 abuildRss(context) { ... }call. There is no equivalent for the deprecatedProperties-based overload — setrssLicense,rssSecServerUrl,rssWorkflowEngineUrlexplicitly on the builder instead. - Keep a reference to the returned
Rssinstance (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 throwsIllegalStateExceptionif 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: String | now a property, not a method |
boolean isEnrolled() | fun isEnrolled(): Boolean | unchanged behavior |
boolean isSessionValid() | fun isSessionValid(): Boolean | unchanged 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(): RssService | still 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): Boolean | now declares @Throws(InvalidSessionException::class) |
Two new checked exceptions are introduced across DssService:
InvalidSessionException— thrown when the session backing theDssServiceis no longer valid.AliasNotFoundException(ch.sysmosoft.sense.android.core.signature) — thrown bygetPendingRequestswhen the givendssProfileIddoesn'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.
6. Recommended error handling: runCatching
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 fromGlobalScope. - Wrap the suspend call(s) in
runCatching { }rather thantry/catch; it keeps error handling declarative and composable (.map,.recover,.getOrElse, etc.) and avoids accidentally swallowingCancellationExceptionthe way a broadcatch (e: Exception)would. - Use a
whenover the caughtThrowableto branch on the specific documented exception types (see the@Throwsannotation 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 aSingle/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.idWhat to change:
- Replace
DssProfile.builder()....build()calls with the data class constructor (SDK consumers normally only read profiles returned byDssService, so this mainly affects test code / fakes). - Replace
profile.getX()accessor calls withprofile.xproperty access. - A new field,
identificationStatus, was added (enumDssProfile.IdentificationStatus:VALID,IDENTIFICATION_REQUIRED,IDENTIFICATION_RENEWAL_REQUIRED). Any code constructing aDssProfilemanually (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.transactionNumberWhat to change:
- The already-deprecated singular accessors
getDigestId()andgetDocumentHash()(deprecated since2.5.0) have been removed. UsedigestIds.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 thetransactionNumberproperty (backed by the sameTransactionNumberUtils.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
minSdkto29if 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 withval rss = buildRss(context) { rssLicense = ...; rssSecServerUrl = ... }and keep a reference torssfor subsequent calls. - Replace
intcomparisons againstRss.DEVICE_COMPLIANT/Rss.ERR_NO_HARDWARE/Rss.ERR_BIOMETRIC_NOT_ENROLLEDwith awhenoverRss.DeviceCompliance. - Replace
sessionService.getUsername()withsessionService.userName. - Replace every
.subscribe(onSuccess, onError)call on aSingle/Completablereturned by the SDK with a coroutine launch +runCatching { }.onSuccess { }.onFailure { }(see section 6). - Add
when/isbranches for the newInvalidSessionException(thrown byRssServiceandDssService) andAliasNotFoundException(thrown byDssService.getPendingRequests). - Update calls to
dssService.getProfile(id)anddssService.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 newidentificationStatusfield forDssProfile. - Replace any use of the deprecated
PendingRequest.getDigestId()/getDocumentHash()withdigestIds.first()/documentHashes.first(). - Remove direct dependencies on internal
client/utilclasses, if any, and use the public interfaces instead. - Remove any now-unused direct dependency on
io.reactivex.rxjava2:rxjavathat was only present because of the old SDK.
10. Appendix — full API mapping
| Area | Old (Java) | New (Kotlin) |
|---|---|---|
| Entry point | Rss.initialize(context) | buildRss(context) { ... } |
| Entry point | Rss.initialize(context, properties) (deprecated) | (removed — configure via builder properties) |
| Entry point | Rss.configuration() (static) | rss.configuration() |
| Entry point | Rss.isInitialized() (static) | rss.isInitialized() |
| Entry point | Rss.getSessionService(token) (static) | rss.getSessionService(token) |
| Entry point | Rss.release() (static) | rss.release() |
| Entry point | Rss.isDeviceCompliant(context) → int | rss.isDeviceCompliant(context) → Rss.DeviceCompliance |
| Entry point | Rss.getEnrolledUsers(context) (static) | rss.getEnrolledUsers(context) |
| Entry point | Rss.disenrollUser(context, username) (static) | rss.disenrollUser(context, username) |
| SessionService | Single<RssService> openSession(char[]) | suspend fun openSession(CharArray?): RssService |
| SessionService | getUsername(): String | val userName: String |
| SessionService | isEnrolled(), isSessionValid(), closeSession(), disenroll(), getRssService() | unchanged signatures, now Kotlin; disenroll()/getRssService() document @Throws |
| RssService | Single<FirebaseMessagingOptions> getFirebaseMessagingOptions() | suspend fun getFirebaseMessagingOptions(): FirebaseMessagingOptions |
| RssService | Completable registerPushNotificationToken(token) | suspend fun registerPushNotificationToken(token: String) |
| RssService | addSessionStatusListener, getSmartContractService, getDssService | unchanged signatures, now throw InvalidSessionException |
| DssService | Single<List<DssProfile>> getAvailableProfiles() | suspend fun getAvailableProfiles(): List<DssProfile> |
| DssService | (none) | suspend fun getAllProfiles(): List<DssProfile> (new) |
| DssService | Single<DssProfile> getProfile(id) | suspend fun getProfile(id): DssProfile? (nullable) |
| DssService | Single<DssProfile.RegistrationStatus> getRegistrationStatus(id) | suspend fun getRegistrationStatus(id): DssProfile.RegistrationStatus |
| DssService | Completable activateProfile(activity, id) | suspend fun activateProfile(activity, id) |
| DssService | Completable disableProfile(id) | suspend fun disableProfile(id) |
| DssService | Single<PendingRequest> getPendingRequest(profileId, requestId) | suspend fun getPendingRequest(profileId, requestId): PendingRequest? (nullable) |
| DssService | Single<List<PendingRequest>> getPendingRequests(profileId) | suspend fun getPendingRequests(profileId): List<PendingRequest> |
| DssService | Completable signPendingRequest(activity, request) | suspend fun signPendingRequest(activity, request) |
| DssService | boolean isBiometricIntegrityValid(id) | fun isBiometricIntegrityValid(id): Boolean (throws InvalidSessionException) |
| Exceptions | DssException(errorCode) / getErrorCode() | DssException(errorCode) / errorCode (unchanged behavior) |
| Exceptions | (none / generic IllegalStateException) | InvalidSessionException (new) |
| Exceptions | (none) | AliasNotFoundException used in getPendingRequests (newly surfaced) |
| Model | DssProfile.builder()....build() + getters | DssProfile(...) data class + properties; new identificationStatus field |
| Model | PendingRequest.builder()....build() + getters | PendingRequest(...) data class + properties (with defaults) |
| Model | PendingRequest.getDigestId() (deprecated) | (removed — use digestIds.first()) |
| Model | PendingRequest.getDocumentHash() (deprecated) | (removed — use documentHashes.first()) |