Android SDK Integration
Integration guide for the SmartOTP Android SDK, version 0.4.0. Written in Kotlin; the entry point is the SdkSoft object.
Installation
Extract Cake_SOTP_SDK_v0.4.0_1.zip into your project — in the example project it goes under a libs folder.
repositories {
maven { url rootDir.path + "/libs/" }
}implementation "xyz.be.cake.smartotpsdk:legacy:0.4.0"Initialize
SdkSoft.initialize(
context = this,
baseURL = "<BASE_URL>",
networkConfig = NetworkConfig(),
isDebug = false) // never ship a release build with request tracing onParameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
context | Context | Yes | — | Android context. |
baseURL | String | Yes | — | Base URL for SDK API calls. |
networkConfig | NetworkConfig | Yes | — | Network client configuration. |
isDebug | Boolean | No | false | Debug mode. Set true to trace request logs. |
NetworkConfig
class NetworkConfig(
val readTimeoutSeconds: Long = 60,
val writeTimeoutSeconds: Long = 60,
val connectTimeoutSeconds: Long = 60,
val callTimeoutSeconds: Long = 0
)| Param | Type | Required | Default | Description |
|---|---|---|---|---|
readTimeoutSeconds | Long | No | 60 | Read timeout for new connections. |
writeTimeoutSeconds | Long | No | 60 | Write timeout for new connections. |
connectTimeoutSeconds | Long | No | 60 | Connect timeout for new connections. |
callTimeoutSeconds | Long | No | 0 | Timeout for complete calls. |
These map onto OkHttpClient settings.
Custom headers
SdkSoft.setRequestHeaders(
mapOf(
Pair("key", "value")
)
)| Param | Type | Required | Default | Description |
|---|---|---|---|---|
headers | Map<String,String> | Yes | — | Extra headers added to every SDK API request. |
Activate
Call after Register succeeds on your backend.
val activateBody = ActivateBody(
partnerId = "{{partner_id}}",
userId = "user_00000001",
username = "nguyenvana",
deviceId = "device_00000001",
deviceOs = "Android",
appVersion = "1.0.0",
registrationId = "reg_00000001",
activationKey = "fake_activation_key",
token = "fake_device_token",
pushToken = "fake_push_token",
)
SdkSoft.activate(activeBody = activateBody, overrideBaseUrl = overrideUrl, callback = {
// Don't log the response — it carries the client secret key.
})ActivateBody
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
partnerId | String | Yes | — | The partner id. |
deviceId | String | Yes | — | The device UUID. |
userId | String | Yes | — | The user id. |
username | String | No | — | The username. |
pushToken | String | Yes | — | Firebase token used to push the challenge key from server to client. |
token | String | Yes | — | The registration token from Register. |
registrationId | String | Yes | — | The registration id from Register. |
activationKey | String | Yes | — | The activation key from Register. Single-use — call Register again for a fresh one. |
deviceOs | String | Yes | — | Device OS for tracking, e.g. Android. |
appVersion | String | Yes | — | Current app version. |
Call parameters
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
activateBody | ActivateBody | Yes | — | The activation payload. |
overrideBaseUrl | String | No | — | Base URL to override for this call. |
callback | Unit | Yes | — | Receives an ActivateResponse. |
Response
{
"success": true,
"code": 1,
"message": "success",
"data": {
"key": "fake_client_secret_key",
"period": "15",
"algorithm": "HmacSHA512",
"length": "6",
"type": "totp",
"issuer": "SOTP",
"id": "123456"
}
}| Field | Type | Description |
|---|---|---|
key | String | The secret key used to generate an OTP. |
period | String | Seconds an OTP stays valid. |
length | String | Number of digits in an OTP. |
issuer | String | The issuer that generated the OTP. |
type | String | The type of soft OTP. |
id | String | The activation ID, also called the serial number. |
The SDK hands this back as ActivateResponse, with key, period, algorithm, length, type, issuer, code, message, id and unhandledException.
INFO
period and length come back as strings here, while Sync Time returns the same two fields as integers. Parse defensively.
| Code | Description |
|---|---|
3 | Parameter invalid |
103 | Thông tin Partner không chính xác — Partner details are incorrect. |
123 | Yêu cầu vượt giới hạn. Vui lòng thử lại sau ít phút — Request limit exceeded. |
124 | OTP không hợp lệ — Invalid OTP. |
125 | OTP hết hạn sử dụng — OTP expired. |
129 | Token không hợp lệ — Invalid token. |
-2 | Secret token decryption failed. |
PIN and Storage
SdkSoft.storedSecureSmartOTP(pin = "1234") // Boolean
SdkSoft.isValidPin(pin = "1234") // Boolean| Param | Type | Required | Default | Description |
|---|---|---|---|---|
pin | String | Yes | — | SmartOTP PIN, 4 digits. |
storedSecureSmartOTP returns true when the secret was stored successfully. Call it once after activation.
Change PIN
val result = SdkSoft.changePin(oldPin = oldPin, newPin = newPin)
when (result) {
ChangePinResult.UNKNOWN_FAILURE -> {}
ChangePinResult.INVALID_OLD_PIN -> {}
ChangePinResult.SUCCESS -> {}
}| Field | Type | Description |
|---|---|---|
SUCCESS | ChangePinResult | The PIN was changed. |
INVALID_OLD_PIN | ChangePinResult | oldPin does not match the stored PIN. |
UNKNOWN_FAILURE | ChangePinResult | An internal SDK error occurred. |
The new PIN cannot repeat either of the previous two.
INFO
Android returns three ChangePinResult values; iOS returns five, splitting out sameOldAndNewPin and samePreviousPins. On Android both of those collapse into UNKNOWN_FAILURE, so you cannot tell the user precisely why their new PIN was rejected.
Generate OTP
val otp = SdkSoft.generateSmartOTP() // basic
val otp = SdkSoft.generateAdvancedSotp(challengeKey = "fake_challenge_key") // advance| Param | Type | Required | Default | Description |
|---|---|---|---|---|
challengeKey | String | Yes | — | Challenge key from Create Transaction. Advance OTP only. |
Both return a 6-digit string, or an empty string on failure.
Get Status
SdkSoft.getStatus(
userId = "user_00000001",
overrideBaseUrl = overrideUrl,
callback = {
Log.d("TAG", "${it?.code} ${it?.message} ${it?.status}")
}
)| Param | Type | Required | Default | Description |
|---|---|---|---|---|
userId | String? | No | — | The user to check. When null, returns the status of the currently activated user. |
overrideBaseUrl | String | No | — | Base URL to override. |
callback | Unit | Yes | — | Receives a StatusResponse with code, message, status. |
Returns one of NOT_REGISTERED, NOT_CURRENT_USER, NON_ACTIVATED, ACTIVATED, TEMPORARY_BLOCKED. The meaning shifts depending on whether you passed a userId — see Mobile SDK Overview § Status Values.
Sync Time
val syncTimeBody = SyncTimeBody(appId = "app_00000001")
SdkSoft.syncTime(
syncTimeBody = syncTimeBody,
useOtp = false,
overrideBaseUrl = overrideUrl,
callback = { }
)| Param | Type | Required | Default | Description |
|---|---|---|---|---|
syncTimeBody | SyncTimeBody | Yes | — | Carries appId, used for tracking. |
useOtp | Boolean | Yes | — | Submit the current and previous period OTP to unblock an account. The SDK uses the stored PIN to generate them. |
overrideBaseUrl | String | No | — | Base URL to override. |
callback | Unit | Yes | — | Receives a SyncTimeResponse. |
useOtp = true is how a TEMPORARY_BLOCKED account gets unblocked — the pair of consecutive OTPs proves the device still holds the right secret.
| Code | Description |
|---|---|
103 | Thông tin Partner không chính xác — Partner details are incorrect. |
129 | Token không hợp lệ — Invalid token. |
190108 | Tài khoản chưa hoạt động — Account not activated. |
190302 | OTP không chính xác — Incorrect OTP. |
-1 | Thông tin không hợp lệ — Invalid information. |
Retire
SdkSoft.retire(
overrideBaseUrl = overrideUrl,
callback = {
Log.d("TAG", "${it?.code} ${it?.message}")
}
)Deactivates the currently active SmartOTP. See Retire for what this does server-side.
Latest Transaction
SdkSoft.getLatestTransaction(
overrideBaseUrl = overrideUrl,
callback = { }
)Returns a TransactionResponse carrying challengeKey plus the transaction fields — appId, transactionId, transactionDesc, transactionAmount, receiverAccount, senderAccount, currency, type, transactionDate, createdAt, transactionMetadata.
Equivalent to Get Latest Transaction; the same caveat applies about it returning whichever transaction is newest.
Status Polling
SdkSoft.startStatusPolling(
interval = 30,
appId = "app_00000001"
)
SdkSoft.cancelStatusPolling()| Param | Type | Required | Default | Description |
|---|---|---|---|---|
interval | Int | No | 30 | Seconds between status checks. |
appId | String | No | — | App ID used in the sync time call. |
The job checks the SmartOTP status on each tick and runs a sync time whenever the status is ACTIVATED. Always pair it with cancelStatusPolling on teardown.
Accessors
| Field | Type | Description |
|---|---|---|
getRegistrationId() | String | Registration id stored at activation. |
getSerialId() | String | Registered serial ID. Empty if not registered or on error. |
getCurrentUserId() | String? | Registered user ID. Null if not registered or on error. |
getCurrentUsername() | String | Registered username. Empty if not registered. |
isSmartOtpRegistered() | Boolean | Whether this device has registered SmartOTP. |
getTime() | Long | The server timestamp. |
getPeriod() | Int | Current period config in seconds. |
getRemainingValidityTime() | Int | Seconds left on the current OTP. |
INFO
ActivateResponse.length is a String?.
Next Steps
- Mobile SDK Overview — concepts shared with iOS
- Validate OTP — verify the code from your backend
- Error Codes — the full error tables