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. In FlowContext, flowActionHandler and flowStateListener are provided for this purpose.

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
    /** Customized logic to validate the bar code */
    open fun validateBarcode(barcode: Barcode): Boolean = true
}

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) {
  open fun onPasscodeCreated(code: CharArray) = Unit
  open fun onCipherCreated(cipher: Cipher) = Unit
  open fun onPasscodeChanged(newCode: CharArray, oldCode: CharArray) = Unit
  open fun onUnlockWithPasscode(code: CharArray) = Unit
  open fun onUnlockWithCipher(cipher: Cipher) = Unit
  open fun onApplicationReset(application: Application) = Unit
  open fun onAppConfigRetrieved(appConfig: AppConfig) = Unit
  open fun onLogSettingsRetrieved(logSettings: LogSettings) = Unit
  open fun onUsageConsentStatusChange(agreed: Boolean) = Unit
  open fun onApplicationLocked() = Unit
  open fun onFlowFinished() = Unit
}

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. When this happens, both the internal status of the flow component and the client mobile application need to be reset, and client code will usually have special logic after the reset, to display the launcher screen, for example.

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 back 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 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 on this event, so the client code needs to take care of this scenario if some initialization logic depends on it.

onLogSettingsRetrieved

The client policy API will return many items at once, such as the passcode policy, log setting policy, etc. Each time after unlocking the mobile application, the flow component will get the policies from the server side and notify the client code about the log settings. If the client code needs to handle the log-related feature, it can listen for this event.

onUsageConsentStatusChange

When the usage feature is enabled in the application, there will be a usage consent step in the onboarding flow. That step will notify the event that either the user allows or denies usage data collection. Upon receiving this event, the client code has the responsibility to either start or stop the usage service.

onApplicationLocked

When the application is put into the background for a period of time, it may lock and the user must unlock it using the 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)
        }
    }

    override fun onLogSettingsRetrieved(logSettings: LogSettings) {
        val sharedPreferences =
            PreferenceManager.getDefaultSharedPreferences(application)
        val existing =
            sharedPreferences.getString(SAPWizardApplication.KEY_LOG_SETTING_PREFERENCE, "")
        if (existing.isNullOrEmpty()) {
            val editor = sharedPreferences.edit()
            editor.putString(
                SAPWizardApplication.KEY_LOG_SETTING_PREFERENCE,
                logSettings.toString()
            )
            editor.apply()
        }
    }

    override fun onUsageConsentStatusChange(agreed: Boolean) {
        val preferenceManager = PreferenceManager.getDefaultSharedPreferences(application)
        if (agreed) {
            try {
                UsageService.getInstance().startUsageBroker()
                UsageService.getInstance()
                    .eventBehaviorViewDisplayed(
                        "SettingsFragment",
                        "elementID",
                        "onUsageConsentStatusChange",
                        "called"
                    )
            } catch (e: RuntimeException) {
                Log.e(TAG, e.message)
                logger.error(e.message)
            }
        } else {
            UsageService.getInstance().stopUsageBroker()
        }
        preferenceManager.edit().putBoolean(USAGE_SERVICE_PRE, agreed).apply()
    }
}
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);
    }

    @Override
    public void onLogSettingsRetrieved(@NotNull LogSettings logSettings) {
        Log.d(WizardFlowStateListener.class.getSimpleName(), "onLogSettingsRetrieved: " + logSettings.toString());
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(application.getApplicationContext());
        String logString = sp.getString(SAPWizardApplication.KEY_LOG_SETTING_PREFERENCE, "");
        if (logString.isEmpty()) {
            sp.edit().putString(SAPWizardApplication.KEY_LOG_SETTING_PREFERENCE, logSettings.toString()).apply();
        }
    }

    @Override
    public void onUsageConsentStatusChange(boolean agreed) {
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(application.getApplicationContext());
        if (agreed) {
            try {
                UsageService.getInstance().startUsageBroker();
                UsageService.getInstance().eventBehaviorViewDisplayed("SettingsFragment",
                        "elementID", "onUsageConsentStatusChange",
                        "called");
            } catch (RuntimeException e) {
                Log.e(WizardFlowStateListener.class.getSimpleName(), e.getMessage());
                logger.error(e.getMessage());
            }
        } else {
            UsageService.getInstance().stopUsageBroker();
        }
        sp.edit().putBoolean(USAGE_SERVICE_PRE, agreed).apply();
    }
}

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 that 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.


Last update: August 12, 2020