Skip to content

Configuration Provider

The ConfigurationLoader class provides easy and consistent access to configuration data from multiple types of sources. This configuration data can, for example, include parameters required to connect to a back end service such as a server URL and port number.

Enterprise applications often retrieve initial configuration data during the onboarding process. See Onboarding for more information.

A configuration loader acts as a container and processor of configuration providers classes that implement the ConfigurationProvider interface. The SAP BTP SDK for Android supports the following configuration providers for fetching data from a specific source:

  • FileConfigurationProvider - Retrieves configuration from a file bundled with the app
  • ManagedConfigurationProvider - Retrieves configuration from an Enterprise mobility management service such as the SAP Business Technology Platform mobile service for app and device management that pushes data to the device. For more information about that mobile service, see MDM/EMM service
  • DiscoveryServiceConfigurationProvider - Queries the Discovery Service. This is a restful service designed to provide initial configuration. See Discovery Service Overview for more information.
  • JsonConfigurationProvider - Retrieves data from a variety of input sources as JSON data, providing a flexible and extensible input mechanism.
  • CustomConfigurationProvider - A configuration provider that you can build to suit your needs by simply extending the ConfigurationProvider interface.

You can use an ordered list of configuration providers, referred to as a "queue", to initialize ConfigurationLoader. A default queue is used if not overridden. See The Provider Queue for more information.

For provider implementations provided by SAP, see the corresponding sections in the SDK reference.

Creating a ConfigurationLoader

You can initialize a ConfigurationLoader with a set of default parameters and a custom set of callbacks as follows:

class DummyConfigurationLoaderCallback extends ConfigurationLoaderCallback {
    public void onCompletion(ProviderIdentifier providerId, boolean success) {}
    public void onError(ConfigurationLoader configurationLoader,
                        ProviderIdentifier providerId,
                        UserInputs requestedInput,
                        ConfigurationProviderError error) {}
    public void onInputRequired(ConfigurationLoader configurationLoader, UserInputs requestedInput) {}
}

ConfigurationLoader myLoader = new ConfigurationLoader(applicationContext, new DummyConfigurationLoaderCallback());
class DummyConfigurationLoaderCallback : ConfigurationLoaderCallback() {
    override fun onCompletion(providerId: ProviderIdentifier, success: Boolean) {}
    override fun onError(configurationLoader: ConfigurationLoader,
                        providerId: ProviderIdentifier,
                        requestedInput: UserInputs,
                        error: ConfigurationProviderError) {
    }

    override fun onInputRequired(configurationLoader: ConfigurationLoader, requestedInput: UserInputs) {}
}

val myLoader = ConfigurationLoader(applicationContext, DummyConfigurationLoaderCallback())

To obtain configuration data, call loadConfiguration(). This causes the loader to begin processing its queue:

myLoader.loadConfiguration();
myLoader.loadConfiguration()

Although this is the simplest constructor, there are others that enable you to use a non-default queue structure or store retrieved configuration data in a specialized manner to receive queue processing information. See Defining Callback Methods for more information.

Defining Callback Methods

If you need to define a callback, initialize the loader using ConfigurationLoaderCallback. This class includes a callback argument that provides a mechanism for communicating information back to your application, for example when:

  • Queue processing is complete
  • Input is required, or
  • A configuration loader encounters an error

onCompletion

A configuration loader uses onCompletion(ProviderIdentifier providerId, boolean success) to notify the application that queue processing has finished. When this message is sent, providerId contains the ProviderIdentifier of the provider that obtained the configuration data and success is true.

If no providers in the queue successfully obtained data, success is false and providerId is null.

onInputRequired

Some provider implementations require input. For example, in the case of DiscoveryServiceConfigurationProvider, a user's email address or onboarding code is required to look up configuration data from the Discovery Service.

The loader invokes onInputRequired() allowing input to be supplied for any providers that expect input. This callback is invoked upon attempting to process the first provider that expects input. The UserInputs object is a container that holds a ProviderInputs for each provider that is an input required provider. To process the collected inputs, call processRequestedInput() on the instance of the configuration loader.

To process the gathered input, a UserInputs object that is populated with that input, can be supplied to the invoking ConfigurationLoader by calling processRequestedInputs(). If there is no input to supply, processRequestedInputs() must be called with an empty UserInputs object to complete the processing. This will skip all providers. To skip an individual provider, send a UserInputs object that has an empty value for that provider.

The following is an example implementation of ConfigurationLoaderCallback that responds appropriately to a request for input from DiscoveryServiceConfigurationProvider. Comments show sample values of input received by the callback and the response provided to the ConfigurationLoader.

class MyCallback extends ConfigurationLoaderCallback {

    @Override
    public void onCompletion(ProviderIdentifier providerId, boolean success) {
        // Do something on completion
    }

    @Override
    public void onError(ConfigurationLoader configurationLoader,
                        ProviderIdentifier providerId,
                        UserInputs requestedInput,
                        ConfigurationProviderError error) {
        // Do something with the error
    }

    @Override
    public void onInputRequired(ConfigurationLoader configurationLoader, UserInputs requestedInput) {
        // Structure `requestedInput`:
        // ["com.sap.configuration.provider.discoveryservice": ["emailAddress": ""]]

        // Present user with UI to enter an email address (as inputText), or cancel the interaction (indicated with cancel)
        if (requestedInput.containsKey(ProviderIdentifier.DISCOVERY_SERVICE_CONFIGURATION_PROVIDER)) {
            launchEmailDialog("Please enter your email");
        }
    }
}

// Completion handler for launchEmailDialog
public void onFinishDiscoveryServiceDialog(boolean cancelled, String inputText) {
    UserInputs inputs = new UserInputs();

    if (cancelled) {
        loader.processRequestedInputs(inputs);
        return;
    }

    ProviderInputs discoveryInputs = new ProviderInputs();
    discoveryInputs.addInput(DiscoveryServiceConfigurationProvider.EMAIL_ADDRESS, inputText);
    inputs.addProvider(ProviderIdentifier.DISCOVERY_SERVICE_CONFIGURATION_PROVIDER, discoveryInputs);
    loader.processRequestedInputs(inputs);
}
internal inner class MyCallback : ConfigurationLoaderCallback() {

    override fun onCompletion(providerId: ProviderIdentifier, success: Boolean) {
        // Do something on completion
    }

    override fun onError(configurationLoader: ConfigurationLoader,
                        providerId: ProviderIdentifier,
                        requestedInput: UserInputs,
                        error: ConfigurationProviderError) {
        // Do something with the error
    }

    override fun onInputRequired(configurationLoader: ConfigurationLoader, requestedInput: UserInputs) {
        // Structure `requestedInput`:
        // ["com.sap.configuration.provider.discoveryservice": ["emailAddress": ""]]

        // Present user with UI to enter an email address (as inputText), or cancel the interaction (indicated with cancel)
        if (requestedInput.containsKey(ProviderIdentifier.DISCOVERY_SERVICE_CONFIGURATION_PROVIDER)) {
            launchEmailDialog("Please enter your email")
        }
    }
}

// Completion handler for launchEmailDialog
fun onFinishDiscoveryServiceDialog(cancelled: Boolean, inputText: String) {
    val inputs = UserInputs()

    if (cancelled) {
        loader.processRequestedInputs(inputs)
        return
    }

    val discoveryInputs = ProviderInputs()
    discoveryInputs.addInput(DiscoveryServiceConfigurationProvider.EMAIL_ADDRESS, inputText)
    inputs.addProvider(ProviderIdentifier.DISCOVERY_SERVICE_CONFIGURATION_PROVIDER, discoveryInputs)
    loader.processRequestedInputs(inputs)
}

onError

If a provider in the queue encounters an error, the loader invokes the callback:

onError(ConfigurationLoader configurationLoader, ProviderIdentifier providerId, UserInputs requestedInput, ConfigurationProviderError error)

where:

  • configurationLoader is the ConfigurationLoader instance that invoked this callback.
  • providerId contains the ProviderIdentifier of the provider that encountered the error
  • requestedInput is a UserInputs object. This object is best described as a HashMap of HashMap objects where the outer key contains the ProviderIdentifier of the providers that need input, and the outer value is a HashMap describing the needed items for that provider. The inner keys are the keys of required items and the values are null.
  • error contains the ConfigurationProviderError object that describes what went wrong.

The callback can do something in response, such as log or display the error. It can also request new input from the user.

This callback must respond back to the ConfigurationLoader by invoking configurationLoader.processRequestedInputs(UserInputs requestedInputs). The supplied requestedInputs object can contain user input, or be empty, in which case all "input expected" providers are skipped.

After sending this message, the loader moves on to the next provider in its queue if this provider is a "no input expected" provider, otherwise it remains on the current "input expected" provider.

class MyCallback extends ConfigurationLoaderCallback {

    @Override
    public void onError(ConfigurationLoader configurationLoader,
                        ProviderIdentifier providerId,
                        UserInputs requestedInput,
                        ConfigurationProviderError error) {
        if (requestedInput.containsKey(ProviderIdentifier.DISCOVERY_SERVICE_CONFIGURATION_PROVIDER)) {
            launchEmailDialog("Error: Please re-enter your email");
        }
    }

}
internal inner class MyCallback : ConfigurationLoaderCallback() {

    override fun onError(configurationLoader: ConfigurationLoader,
                        providerId: ProviderIdentifier,
                        requestedInput: UserInputs,
                        error: ConfigurationProviderError) {
        if (requestedInput.containsKey(ProviderIdentifier.DISCOVERY_SERVICE_CONFIGURATION_PROVIDER)) {
            launchEmailDialog("Error: Please re-enter your email")
        }
    }

}

The Provider Queue

The alternate constructor, ConfigurationLoader(Context context, ConfigurationLoaderCallback callback, ConfigurationProvider[] providers), allows you to specify your own provider queue. The providers parameter contains the queue of configuration providers.

In the case of ConfigurationLoader constructors that do not have a providers argument, the default queue is constructed.

The default queue is structured as follows:

  1. ManagedConfigurationProvider
  2. FileConfigurationProvider
  3. DiscoveryServiceConfigurationProvider
  4. JSONConfigurationProvider

Alternatively, a queue of a different size or ordering can be constructed by passing your own collection of providers for this parameter:

ConfigurationProvider[] providers = new ConfigurationProvider[]{
        new FileConfigurationProvider(context),
        new DiscoveryServiceConfigurationProvider(context)
};

ConfigurationLoader myLoader = new ConfigurationLoader(context, new MyCallback(), providers);
val providers = arrayOf(FileConfigurationProvider(context), DiscoveryServiceConfigurationProvider(context))

val myLoader = ConfigurationLoader(context, MyCallback(), providers)

In this example, myLoader.providers looks like:

  1. FileConfigurationProvider
  2. DiscoveryServiceConfigurationProvider

CustomProvider

Creating a custom configuration provider allows you to retrieve configuration from an alternate source such as NFC. Implement the ConfigurationProvider interface with your custom implementation. To use a custom configuration provider, initialize your own ProviderQueue containing this new provider and any other predefined providers the ConfigurationLoader should process.

Configuration Persistence

Storing configuration data allows you to persist bootstrapping configuration - for example, for the authentication server - in a permanent location. By storing this data, you only have to call the ConfigurationLoader during the first launch of the application and can then use that persisted data for the entire lifecycle of your application.

Storing Configuration Data

By default the ConfigurationLoader stores configuration in SharedPreferences with the key com.sap.configuration.provider.configurationstore. This behavior can be overridden by implementing the ConfigurationPersister interface and passing it as a parameter when constructing ConfigurationLoader. This could be used for scenarios such as writing the data to a preferred SharedPreferences key or encrypting it before storing.

If the default persistence mechanism has been overridden with a custom implementation of a ConfigurationPersister, that object's persistConfiguration(JSONObject configuration) is called, allowing you to choose where and how your data is persisted. Although the interface does not require you to implement a method to read saved data, you can implement such a method to read the data at a later time. In that regard, the static method, DefaultPersistenceMethod.getPersistedConfiguration(Context context) is provided to retrieve the configuration data stored by the DefaultPersistenceMethod as a JSONObject object.

Retrieving Configuration Data

Process the queue for configuration data by calling loadConfiguration().

myLoader.loadConfiguration();
myLoader.loadConfiguration()

Providers in the queue are examined, in order, for configuration data by calling each provider object. Once processing has been completed successfully and the onCompletion() handler has been called, the retrieved data can now be accessed.


Last update: February 20, 2023