Skip to content

Extension Points

In addition to the default implementation of the predefined flows, the flow component also has extension points that allow client code to insert customized logic at certain points in the flow process. flowActionHandler and flowStateListener are provided for this purpose, as part of FlowContext.

Flow Action Handler

The following code provides the definition of FlowActionHandler. For passcode validation customized logic, FlowActionHandler will be executed after the default rules that have been defined in the passcode policy.

abstract class FlowActionHandler {
    /** Digit-only passcode. */
    open fun isPasscodeDigitOnly(): Boolean = false
    /** Localize digits to Latin or not */
    open fun isLocalizingPasscodeDigitsToLatin(): Boolean = false
    /** Customized logic to validate the passcode */
    open fun validatePasscode(code: CharArray): Boolean = true
    /** Provide AppConfig by resolving barcode content for authentication */
    open fun parsingBarcode(barcode: String): AppConfig? = null
    /**
     * Validates the barcode. If the validation fails, [ServiceResult.FAILURE] should
     * be returned with the error message. If the error message is empty, the default error message
     * defined in [QRCodeReaderSettings] will be used.
     */
    open fun validateBarcode(barcode: String): ServiceResult<Boolean> = ServiceResult.SUCCESS(true)
    /** Certificate provider for certificate challenge **/
    open fun getCertificateProvider(): CertificateProvider = SystemCertificateProvider()
    /** Provide [SslClientAuth] for certificate authentication **/
    open fun onCertificateSslClientAuthPrepared(): SslClientAuth? = null
    /** The obfuscate algorithm for user name, used in sign in screen*/
    open fun obfuscateUserName(name: String) : String = name
    /** The obfuscate algorith for email, used in sign in screen. */
    open fun obfuscateEmail(email:String): String = DeviceUser.obfuscateEmail(email)
    /**
     * Represents the event handler when the **Back** button of the WebView is pressed.
     * This happens when performing authentication that requires a WebView.
     */
    open val onWebViewBackPressed: (() -> Unit)? = null
    /** Provide SslClientAuth for certificate authentication **/
    open fun onCertificateSslClientAuthPrepared(): SslClientAuth? = null

    /**
     * Gets the effective OAuth client from the OAuth config if more than one OAuth client is defined in the
     * application configuration. By default, the first OAuth client in the OAuth config will be the effective one.
     */
    open fun getEffectiveOAuthClient(oAuthConfig: OAuthConfig): AbstractOAuthClient {
        ...
    }

    /** Activates from the managed configuration system. */
    open fun activateFromManagedConfig(bundle: Bundle): AppConfig { ... }
}

Note

getEffectiveOAuthClient is introduced as of SAP BTP SDK for Android version 3.4. In previous versions, a property, effectiveOAuthClientId, in FlowOptions determined the effective OAuth client for the client code to specify by providing the client ID.getEffectiveOAuthClient provides greater flexibility for the client code to do this. The default logic in this function will still honor the property in FlowOptions, if specified, but use the first one otherwise. The client code can override this function to provide other logic to determine the effective OAuth client.

Flow State Listener

When the passcode is created, the flow will provide the passcode to the client code with onPasscodeCreated, so that the client code can use it to encrypt the online/offline OData local store, for example.

abstract class FlowStateListener(private val application: Application) {
/**
 * State handle when the passcode is updated, if [oldCode] is present, or the passcode was changed.
 * Otherwise, the passcode is newly created.
 */
open fun onPasscodeUpdated(newCode: CharArray, oldCode: CharArray?) = Unit
open fun onUnlockWithPasscode(code: CharArray) = Unit
open fun onApplicationReset(application: Application) = Unit
open fun onAppConfigRetrieved(appConfig: AppConfig) = Unit
open fun onApplicationLocked() = Unit
open fun onFlowFinished() = Unit
open fun onClientPolicyRetrieved(policies: ClientPolicies) = Unit
open fun onConsentStatusChange(consents: List<Pair<ConsentType, Boolean>>) = Unit
open fun onUserSwitched(newUser: DeviceUser, oldUser: DeviceUser?) = Unit
open fun onOfflineEncryptionKeyReady(key: String?) = Unit
open fun onOkHttpClientReady(httpClient: OkHttpClient) = Unit
}

For additional information, refer to this technical article on FlowStateListener

Note

All these functions except onFlowFinished will be executed in a thread other than the UI thread, and the client code must handle the exceptions properly. The flow component will ignore them if encountered and only show a message to avoid crashing.

The following state event handles are of particular importance:

onApplicationReset

This will be called when the user wants to reset the application by starting the 'reset' flow. This will clear all the data in the mobile application.

Sample client mobile application:

//Start reset flow
val flowContext = FlowContextRegistry.flowContext.copy(
    flowType = FlowType.RESET,
    flowStateListener = WizardFlowStateListener(application))
Flow.start(this, flowContext)

//WizardFlowStateListener
override fun onApplicationReset(application: Application) {
    application.resetApp()
    //Clear application data first, then return to launcher activity
    application.startActivity(Intent(application, WelcomeActivity::class.java).apply {
        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
    })
}
//Start reset flow
FlowContext flowContext = new FlowContextBuilder()
        .setApplication(application.appConfig)
        .setFlowType(FlowType.RESET)
        .setFlowStateListener(new WizardFlowStateListener(application))
        .build();
Flow.start(this, flowContext);

//WizardFlowStateListener
public void onApplicationReset() {
    this.application.resetApp();
    Intent intent = new Intent(application, WelcomeActivity.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
    application.startActivity(intent);
}

The code will go to WelcomeActivity after reset.

Note

Because a reset can happen when a user forgets their passcode, this flow component does not provide confirmation before executing the flow, therefore providing a confirmation dialog before executing the reset flow is highly recommended.

onAppConfigRetrieved

When the client uses the Discovery Service or QR code for onboarding, only the application id is required in the client code. The complete application information will be retrieved from either the Discovery Service or using the QR code. When the flow gets the complete information, it will notify the client code so that it can handle some initialization logic, for example the online OData library needs the service root URL to initialize the DataProvider. For such cases, the client should provide its own FlowStateListener and listen to the onApplicationRetrieved event to perform the required logic.

Even if the client code does not use Discovery Service, and provides the complete application configuration before onboarding, the flow will also notify the client code about the application configuration using the method described above, so the client code can use the same code for both cases by listening to this event.

Note

Flow will only notify this event for onboarding and restore. For the restore flow, this event will only be notified once, but for onboarding (because users might click the Back button at certain points), there might be multiple notifications for this event, so the client code needs to take care of this scenario if some initialization logic depends on it.

onClientPolicyRetrieved

Each time after unlocking the mobile application, the flow component will get the client policies from the server side and notify the client code. If the client code needs to handle the log-related feature, it can listen for this event.

onConsentStatusChange

Currently there are two services that need the user's consent to collect or upload data. These are UsageService and CrashService. When the user agrees to or denies consent when doing onboarding, the status will be notified to the client code. Please note that, in onboarding, this method will be called multiple times, each time with one consent status inside. However, the next time the mobile application starts, the flows component will read the status saved in the local secure database, and notify the client code about all of them.

onApplicationLocked

When the application is put into the background for a period of time, it may lock and the user must unlock it using a fingerprint or passcode. A Locked event will be notified to the client when this happens. Client code can listen for this event, if desired. For example, when a push notification message is used to bring up the application, the client code may need to know if the application is currently locked or not.

Sample client code:

class WizardFlowStateListener(private val application: SAPWizardApplication) :
    FlowStateListener() {

    override fun onAppConfigRetrieved(appConfig: AppConfig) {
        Log.d(TAG, "onAppConfigRetrieved: $appConfig")
        application.initializeServiceManager(appConfig)
    }

    override fun onApplicationReset() {
        Log.d(TAG, "onApplicationReset executing...")
        this.application.resetApplication()
        Intent(application, WelcomeActivity::class.java).also {
            it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
            application.startActivity(it)
        }
    }
}
public class WizardFlowStateListener extends FlowStateListener {
    private static Logger logger = LoggerFactory.getLogger(WizardFlowStateListener.class);
    public static final String USAGE_SERVICE_PRE = "pref_usage_service";
    private SAPWizardApplication application;

    public WizardFlowStateListener(@NotNull SAPWizardApplication application) {
        super();
        this.application = application;
    }

    @Override
    public void onAppConfigRetrieved(@NotNull AppConfig appConfig) {
        Log.d(WizardFlowStateListener.class.getSimpleName(), "onAppConfigRetrieved " + appConfig.toString());
        application.initializeServiceManager(appConfig);
    }

    @Override
    public void onApplicationReset() {
        Log.d(WizardFlowStateListener.class.getSimpleName(), "onApplicationReset executing...");
        this.application.resetApp();
        Intent intent = new Intent(application, WelcomeActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
        application.startActivity(intent);
    }
}

onFlowFinished

The flows framework will send this event to the client code when a flow finishes successfully and the flow activity is removed from the back stack, and this function will be executed in the main thread.

Usually the client code will start a flow from either an activity or a fragment, then monitor the flow finish status with onActivityResult. But there are also cases when the client code might not know the activity that starts the flow, for example using the topmost activity in the back stack to start a flow. In such cases, putting onActivityResult into every possible activity is not feasible. In this case, the onFlowFinished callback can be used instead.

override fun onFlowFinished() {
    application.notificationMessage?.let {
        NotificationUtilities.showNotificationMessage(it)
    }
}
@Override
public void onFlowFinished() {
    if(application.notificationMessage != null){
        NotificationUtilities.showNotificationMessage(application.notificationMessage);
    }
}

Note

This callback will only be invoked when the flow is successfully completed. If at any time the flow is canceled, for example, clicking the Back button to exit the flow activity, this callback will not be invoked.

Note

As of SAP BTP SDK for Android version 3.4, this callback will NOT be called if the flow is started from the API with a callback function provided. See Start Flow for details.

onUserSwitched

While this callback function sounds like it would only work in multiple user mode, it actually also works in single user mode. The callback function has two arguments: one is the new user, the other is the previous user, which is either null to indicate that this is the very first user onboarded, or the previous user id. If these two arguments have the same value, it means the previous user has returned to this device and no actual user switch has occurred.

This event will be notified to client code after authentication is performed in the onboarding process, or after the restore flow has completed.

    override fun onUserSwitched(newUser: DeviceUser, oldUser: DeviceUser?) {
        logger.info(String.format("User switched to %s", newUser.id))
        application.currentUserId = newUser.id
        oldUser?.also {
            logger.debug(String.format("Old user id %s", it.id))
            if (newUser.id != it.id) {
                PreferenceManager.getDefaultSharedPreferences(application).apply {
                    edit().putBoolean("offline_need_init", true)
                        .apply()
                }
                application.sapServiceManager?.close()
            }
        }
    }
    @Override
    public void onUserSwitched(
            @NotNull DeviceUser newUser, @Nullable DeviceUser oldUser
    ) {
        logger.debug(String.format("new user: %s", newUser.getId()));
        application.currentUserId = newUser.getId();
        if(oldUser != null && newUser.getId().equals(oldUser.getId())) {
            PreferenceManager.getDefaultSharedPreferences(application)
                    .edit()
                    .putBoolean("offline_need_init", true)
                    .apply();
            application.getSAPServiceManager().close();
        }
    }

onOfflineEncryptionKeyReady

This callback is only used for offline OData mobile applications. The offline library requires an encryption key and the current user id to open the offline database. When doing an onboarding or restore flow, the flows component will call the server API to get the encryption, then enforce it with other information and return the enforce encryption key to the client code. For a single user on the device, the encryption will remain unchanged within this callback.

This callback will always be called after onUserSwitched, so the current user id will be available when this function is called.

The encryption key might be null because the server side might not enable this feature. When this happens, the client code needs to generate a key by itself to open the offline database.

The flows component only calls the server API when SharedDeviceService is initialized with SDKInitializer.

    override fun onOfflineEncryptionKeyReady(key: String?) {
        //pass the key into Worker, then to SAPServiceManager
        //user id is in this class.
        logger.info("offline key ready.")
        application.currentUserId?.also {
            OfflineOperationHelper(application).createIniSyncWorker(it, key)
        } ?: error("Current user id not set yet.")
    }
        @Override
    public void onOfflineEncryptionKeyReady(@Nullable String key) {
        //pass the key into Worker, then to SAPServiceManager
        //user id is in this class.
        logger.info("offline key ready.");
        OfflineOperationHelper(application).createIniSyncWorker(application.getCurrentUserId(), key);
    }

onOkHttpClientReady(httpClient: OkHttpClient)

For the onboarding and restore flows, the flows component will prepare OkHttpClient at the appropriate time and notify the client code using this callback so that the client code can update it, to add another interceptor, for example.

override fun onOkHttpClientReady(httpClient: OkHttpClient) {
    //With 'save = true', 'client' will be saved back into ClientProvider
    //after adding the interceptor
    val client = httpClient.addUniqueInterceptor(APIKeyInterceptor("apikey"), save = true)

    //If new properties are added to 'client', and the new HttpClient is intended to be used
    //in subsequent places, make sure to save it back into ClientProvider
    val newClient = client.newBuilder()
        .connectTimeout(7_000, TimeUnit.MICROSECONDS)
        .callTimeout(500, TimeUnit.MICROSECONDS)
        .build()
    ClientProvider.set(newClient)
}
@Override
public void onOkHttpClientReady(@NotNull OkHttpClient httpClient) {
    //With the last parameter as 'true', 'client' will be saved back into ClientProvider
        //after adding the interceptor
        OkHttpClient client = SDKUtils.addUniqueInterceptor(httpClient, chain -> {
            Request request = chain.request();
            Request newRequest = request.newBuilder()
                    .header("my_header", "my_header_value")
                    .build();
            return chain.proceed(newRequest);
        }, true);

        //set other properties
        OkHttpClient newClient = client.newBuilder()
                .connectTimeout(1000, TimeUnit.MICROSECONDS)
                .callTimeout(500, TimeUnit.MICROSECONDS)
                .build();

        //set it into ClientProvider. This is required if new properties are needed
        //and 'newClient' will be used for API calls after this point in other places.
        ClientProvider.set(newClient);
}

Last update: June 14, 2022