Terms Of Use

To use the Favendo SDKs, at least one of the versions of the Android or iOS operating systems supported by Google or Apple is required. With other or older mobile operating systems, correct use of the SDKs is no longer guaranteed. Please find our General Terms and Conditions here.

Setup

To use Backspin in your Android application you have to add our Maven repository to your projects build.gradle file.

project build.gradle
allprojects {
    repositories {
        google()
        jcenter()
        maven {
          url "http://maven-repository.favendo.de/artifactory/backspin-android-sdk"
          credentials {
              username = "${favendo_artifactory_user}"
              password = "${favendo_artifactory_password}"
          }
          name = "plugins-favendo"
        }
    }
}

In order to use our repository you need to authenticate with a valid username and password. It is a good practice to declare those as variables by adding the following lines to your project’s gradle.properties file.

gradle.properties
favendo_artifactory_user = your_name
favendo_artifactory_password = your_password

Now you have to add the dependency to your module’s build.gradle file.

module build.gradle
dependencies {
    implementation 'com.favendo.android:backspin-sdk:6.0.0'
}

The Backspin SDK requires several permissions to operate. Your app will not work unless you add the following lines to your AndroidManifest.xml file:

AndroidManifest.xml
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
The Backspin SDK is compiled with the following options:
gradle.properties
android {
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_1_8.toString()
    }
}

It would be a good idea to define these in the project level build.gradle to avoid running into issues with Java 8 features.

Android 5.0 and above require runtime permissions for ACCESS_FINE_LOCATION. For more information see the documentation on Android Developers.
Android 10 and above require the new runtime permission ACCESS_BACKGROUND_LOCATION for scanning in the background. For more information see the documentation on Android Developers.
Android 12 and above require the new runtime permissions BLUETOOTH_SCAN and BLUETOOTH_CONNECT for scanning BLE devices and accessing their information respectively. For more information see the documentation on Android Developers.

Initialization

Before you can use any feature of the SDK you have to initialize it. This requires a Base64 encoded authentication key as well as the type of server the SDK should connect to. Both can be set with the help of the ConnectionConfig class.

ConnectionConfig config = new ConnectionConfig(authKey, serverType)
        .setLanguage("de")                    // optional
        .setCloudRegistrationId(cloudRegId)   // optional
        .setUser(User.from(token));             // optional

You can use the optional setLanguage() method to specify the language of the downloaded data. If not set the device’s language will be used.

If you want to receive push notifications you have to request a cloud registration ID from the Google Play Services and pass it to the setCloudRegistrationId() method. For more information see the documentation on Android Developers.

Use setUserToken to specify the token that identifies the current user for asset tracking. Look at Asset Tracking for more details.

You can now pass your created ConnectionConfig object to the BackspinSdk.init() method of the SDK. Additional parameters are an Android Context and a RootVenuesLoadedListener which gets called as soon as the registration at the backend was successful.

BackspinSdk.init(context, config, new RootVenuesLoadedListener() {
    @Override
    public void onRootVenuesLoaded(List<RootVenue> rootVenues) {
        // choose one of the root venues and continue with the Data.load method
    }

    @Override
    public void onError(DataError error) {
        Log.e(TAG, error.getMessage(context));
    }
});

As a parameter of the callback method you receive the complete list of all available root venues. Root venues are logical units that divide the system in separate groups. Each one has its own data like beacons, venues, offers and more.

To get access to that data you have to choose a specific root venue and pass it to the BackspinSdk.Data.load() method along with a DataLoadedListener.

BackspinSdk.Data.load(rootVenue, new DataLoadedListener() {
    @Override
    public void onSuccess() {
        // you are ready to work with the SDK now
    }

    @Override
    public void onError(DataError error) {
        Log.e(TAG, error.getMessage(context));
    }
});

When onSuccess() is called all data of the specified RootVenue is downloaded and locally available on the device. Subsequent calls to load the RootVenue will now work offline as well.

If your use case uses only one root venue you can simply use the initWithDefaultRootVenue(). This will automatically call the BackspinSdk.Data.load() method for the first root venue. A complete example will look like this:

ConnectionConfig config = new ConnectionConfig(authKey, urlType);
BackspinSdk.initWithDefaultRootVenue(context, config, new DataLoadedListener() {
    @Override
    public void onSuccess() {
        // you are ready to work with the SDK now
    }

    @Override
    public void onError(DataError error) {
        Log.e(TAG, error.getMessage(context));
    }
});

User specific information

In order to work with user related data you need to specify a User object. You can either do this with the ConnectionConfig or later by calling the setUser method of the BackspinSdk.

BackspinSdk.setUser(User.from(token));
Be aware that if you change the user token some of the other features like subscriptions will restart or stop and all listeners you have set will be abandoned. This means you have to re-subscribe in case of subscriptions.

Data

This chapter is dedicated to the Backspin data model and how to obtain, work with and finally upload data to the backend. Data is stored remotely and cached locally. You don’t have to bother with local or remote data or the availability of network while using the SDK. Simply use the API methods and it takes care of everything else.

Scoped Data

When dealing with large amounts of data it is necessary to organize them in chunks so that only the currently necessary parts are loaded. Therefore the data is divided in scopes. For example, if the application you write is used for multiple shopping centers, every shopping center would be its own scope. When you have initialized the SDK correctly you have already chosen a certain scope (a root venue) which is now available locally. To change the scope simply call the BackspinSdk.Data.load() method with the new root venue. If you want to refresh the current scope’s data use the BackspinSdk.Data.reload() method. You can force loading by setting the force parameter to true.

When switching to a root venue (by calling BackspinSdk.Data.load()) that has been loaded already an internet connection is not required. Additionally you can check whether a certain RootVenue is loaded by using the BackspinSdk.Data.isCached() method. Keep in mind that cached data is not necessarily up to date.

After loading the right root venue you can start to obtain data belonging to it.

// retrieve all venues of the current scope from the local database
BackspinSdk.Data.getVenues(new DataLoadedResultListener<List<Venue>>() {
    @Override
    public void onSuccess(List<Venue> result) {
        ...
    }

    @Override
    public void onError(DataError error) {
        ...
    }
});

// retrieve a single beacon with a specific ID
Beacon beacon = BackspinSdk.Data.getBeacon(beaconId);

In this manner all scope bound Backspin entity types can be obtained: Beacon, Venue, VenueOffer, VenueCategory, Level and more.

The account bound types are discussed in the next chapter.

Includes

To save traffic and performance the SDK allows you to include only specific types you are interested in. You can do this with the help of the ConnectionConfig. Let’s say you want to receive beacon data and the navigation graph only.

ConnectionConfig connectionConfig = new ConnectionConfig(authKey, urlType)
    .includeBeacons()
    .includeNavigationGraphs();
This means that the BackspinSdk.Data.getLevels(), BackspinSdk.Data.getVenues() and the other methods besides beacons and navigation graphs will return an empty collection.

Excludes

You can also exclude specific properties from the incoming data. So if you want all root scope data but the venues should not have their VenueLocation information loaded you can use the excludePropertiesFromVenues() method.

ConnectionConfig connectionConfig = new ConnectionConfig(authKey, urlType)
    .excludePropertiesFromVenues(Venue.VenueLocations);

Additionally you can use the overload of the include…​(propertiesToExclude) method to specify that you only want these objects but without a specific property. Therefore the following example requests only the levels from the backend but without their level plans.

ConnectionConfig connectionConfig = new ConnectionConfig(authKey, urlType)
    .includeLevels(Level.LevelPlan);
As beacons are one of the core features of the SDK you should be aware that excluding them means that all modules that depend on beacon data will not work anymore.

Explicit Loading

Sometimes it makes sense to load or refresh only a specific model type. Especially when you work with includes or excludes. The following snippet requests all levels of the current root venue from the backend and puts the data into the local database.

BackspinSdk.Data.loadLevels(new DataLoadedListener() {
    @Override
    public void onSuccess() {
        List<Level> levels = BackspinSdk.Data.getLevels();
    }

    @Override
    public void onError(DataError error) {
        Log.e(TAG, error.getMessage(context));
    }
})

Furthermore you can pass a RootVenue object as the first parameter if you want to load the data of a specific root venue.

Loading Images

Data like Venue, VenueOffer, VenueCategory or LevelPlan provide methods to build a URL which can be used to request the corresponding pictures from the Backspin backend. You can request JPG or PNG encoded images and logos in a specific size.

// the larger side of the image will have 1920 pixels
int largestDimensionInPixel = 1920;
String imageJpgUrl = venue.buildImageUrlJpg(largestDimensionInPixel);
String logoPngUrl = venue.buildLogoUrlPng(largestDimensionInPixel);

The Backspin SDK does not come with a build-in image loader. Instead you can choose whatever library you like. For example Glide or Picasso are a good choice. For most of the image loading libraries it will look similar to this:

Glide.with(this).asBitmap().load(imageJpgUrl).into(imageView);

Picasso.get().load(logoPngUrl).into(imageView);

Connection Specifications

The Backspin SDK has a minimum Android API level of 21 which means that there are still some old and slow devices we support. So there are 2 important things to consider when downloading images from the Backspin backend.

As the Backspin Backend only allows TLS 1.2 connections you have to enable these lollipop devices manually. We added a simple helper method which creates the according SSLContext. Most of the image loading libraries will offer a method to add this or its socket factory to the underlying http client.

// this creates a SSLContext class which enforces TLS1.2
SSLContext sslContext = Tls12SocketFactory.createTls120SSLContext()

// if you are using OkHttp3 you can use this even simpler method by passing the whole client builder object
OkHttpClient.Builder builder = Tls12SocketFactory.enableTls12OnPreLollipop(builder);
If the device has no TLS1.2 at all it will not connect to the Backspin backend. For most devices you will than receive an handshake exception and will not be able to use the SDK on that device.

The other thing is about the http timeouts. There are situations where the device has bad internet connection or the location where the indoor app runs has no or only few places with wifi. Whenever you want to increase the timeout of http calls in your app it is NOT sufficient to call connectionConfig.setHttpClientTimeoutMillis. This will set the http timeout only for the requests triggered by the SDK. So you also have to do this for your image loading http client too. A full example of the Glide configuration will look like this:

public class MyGlideModule implements GlideModule {
    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
    }

    @Override
    public void registerComponents(Context context, Glide glide) {
        // configure timeouts for image loading
        OkHttpClient.Builder builder = new OkHttpClient.Builder()
                .connectTimeout(8, TimeUnit.SECONDS)
                .readTimeout(8, TimeUnit.SECONDS);

        // enable TLS 1.2 on Android 5 devices (only the devices which have TLS 1.2 support)
        Tls12SocketFactory.enableTls12OnPreLollipop(builder);

        glide.register(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory(builder.build()));
    }
}

Scanning for Beacons

As a core functionality of the SDK you can use the BLE scanning to look for nearby beacons. There are four other modules that depend on the scan results: Position, Proximity, Zone and Notification. This should be considered when starting and stopping the scan process.

Start & Stop

To receive beacon scan results periodically you just have to call the start() method of the Scan module.

BackspinSdk.Scan.start();

Similarly you have to call the stop() method to finish the beacon scan.

BackspinSdk.Scan.stop();

As the scanning for beacons is a very battery-intensive operation. You should think about stopping the process whenever you don’t need it. But be aware that calling stop() means that the dependent modules will also receive no scan results anymore.

As best practice you should take advantage of android’s lifecycle methods. For example the activity’s onStart() method to start the scan process and onStop() to stop it.

Listen for Updates

To get updates of the scan process you can simply add a ScanUpdateListener. Whenever the Scan module is started you will receive beacon scan results periodically.

BackspinSdk.Scan.addUpdateListener(new ScanUpdateListener() {
    @Override
    public void onScanUpdate(List<BeaconScanResult> beaconScanResults) {
        Log.i(TAG, beaconScanResults.size() + " beacons received");
        // gets called on background thread - use runOnUiThread() if needed
    }
});

Note that adding a listener is only necessary if the scan results are of relevance to you - modules like Position or Proximity register their own ScanUpdateListener.

The default scanning only cares for beacons that are known to the backend and belong to the currently active scope. If you want to receive all beacons including those that are not known to the backend you can use BackspinSdk.Scan.addRawUpdateListener().

Frequency

There are two different scan frequencies in the SDK. If ScanFrequeny is set to ScanFrequency.HIGH, the system scans for beacons continuously and the ScanUpdateListener is called every second. ScanFrequency.LOW represents a battery saving mode which scans only in an configurable interval (LowFrequencyScanInterval). ScanFrequency.HIGH is used by default. You can change the frequency at any time by calling:

BackspinSdk.Scan.setFrequency(ScanFrequency.HIGH|LOW|AUTO);

In many use cases it makes sense to do high frequent scanning when the app is in the foreground and switch to low frequency mode when the app goes to the background. You can use ScanFrequency.AUTO to achieve this behavior. In order for the SDK to be able to monitor your app’s activities you need to add the following line to your custom Application class. This will also set the scan frequency to ScanFrequency.AUTO

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        BackspinSdk.Scan.initAutoFrequency(this);
    }
}
Although the low frequent scanning is more battery friendly, it is necessary to stop the scanning when the user no longer needs it.

Configuration

There are several settings to adjust the beacon scanning for your specific environment.

BackspinSdk.Scan.getConfig()
    .setRssiEstimationHistorySize(...)
    .setScanHistorySize(...)
    .setBeaconTimeout(...)
    .setUsingCryptoBeacons(...);
RssiEstimationHistorySize

Sets the amount of measurements that are used to calculate a smoothed/estimated RSSI value for each scanned beacon. A higher estimation history size results in a more stable RSSI value, but it also reduces adjustment speed.

ScanHistorySize

Sets the amount of scan updates the SDK remembers before it throws old ones away. In most cases, this value should be left at default.

BeaconTimeout

Sets the time that has to pass before a beacon scan result is considered invalid/outdated. This timeout compensates the circumstance that many devices only scan a beacon every few seconds. It means that a recently scanned beacon is still used for calculations, even if the last concrete scan if it is already some time in the past.

CryptoBeaconsSeed

Sets the seed number which is used for the V1 beacon decryption algorithm. This is only needed if the beacon installation consists of CryptoBeacons.

LowFrequencyScanInterval

Sets the time in ms that passes between two scans when in low frequency mode. Be aware that a shorter interval leads to more battery drain!

BeaconBatteryScanEnabled

Enables beacon battery scans. If enabled, the SDK will scan and upload beacon battery states when the beacon scanning of the scan module is enabled.

BeaconBatteryScanIntervalMillis

Sets the interval at which the SDK will scan for beacon battery state scans.

BeaconBatteryScanDurationMillis

Sets the duration of the beacon battery state scan.

Please make sure to stop the scanning before making changes to the configuration. This ensures that all settings are applied properly.

Transmitting as a Beacon

The SDK also supports transmitting as a beacon. A device must have Android 5.0+, a Bluetooth LE chipset that supports peripheral mode, and a compatible hardware driver from the device manufacturer.

Use the following code snippet to check if your device is able to transmit as a beacon:

boolean deviceSupportsAdvertising = BackspinSdk.Scan.isAdvertisingSupported();

If this call returns true you are ready to use the startAdvertising method. Use any valid UUID, major and minor ID you want:

BackspinSdk.Scan.startAdvertising("bc589fc1-2e35-4f21-b6d6-026ec215df9a", 1, 1);

If advertising is not needed anymore you can simply call BackspinSdk.Scan.stopAdvertising().

Beacon Battery scanning

Another feature of the SDK is the BLE scanning for beacon battery stats. This feature is only supported in Android SDK version 21 or higher. To save battery this feature is disabled as default. To enable the beacon battery scanning just set the config as follows:

BackspinSdk.Scan.getConfig().setBeaconBatteryScanEnabled(true)

If the beacon battery scanning is enabled it starts and stops together with the normal scanning for beacons. The default scan interval is 10 Minutes and the scanning duration is 6 Seconds. To change the interval and duration just change set the beacon battery scan interval or duration in millis as follows:

BackspinSdk.Scan.getConfig().setBeaconBatteryScanIntervalMillis(600000)
BackspinSdk.Scan.getConfig().setBeaconBatteryScanDurationMillis(10000)

After the scan interval is reached the scanned beacon battery stats will be uploaded to the server. If the upload is successful the local stored stats will be deleted and a new scan will start for the duration that is set in the config. This process repeats till the beacon scanning stops. If the upload was not successful it will automatically retry when the next interval is reached.

Positioning

One of the main features of the SDK is to calculate an indoor position based on scanned beacon signals. Zone and Notification depend on position updates. This should be considered when starting and stopping the positioning. Be aware that the positioning needs a started Scan module in order to post updates.

Start & Stop

To start the SDK’s position calculation call the start() method of the Position module.

BackspinSdk.Position.start();

Similarly you have to call the stop() method to end the calculation.

BackspinSdk.Position.stop();
As best practice you should take advantage of android’s lifecycle methods. For example the activity’s onStart() method to start the positioning and onStop() to stop it.

Listen for Updates

To receive the latest position periodically you have to add a PositionUpdateListener.

BackspinSdk.Position.addUpdateListener(new PositionUpdateListener() {
    @Override
    public void onPositionUpdate(Position position) {
        Log.i(TAG, "calculated a new position: " + position);
        // gets called on background thread - use runOnUiThread() if needed
    }
});

In many use cases it makes sense to snap the received position to the navigation graph before displaying it on a map. Look at Path Snapping for details.

You can also request the latest position by calling BackspinSdk.Position.getLatest(). Be aware that it will return null if no position has been calculated yet.

Configuration

There are several settings to adjust the position calculation for your specific environment. In many use cases the predefined defaults already provide a reasonable result.

BackspinSdk.Position.getConfig()
    .setPositionCalculationInterval(...)
    .setSensibleRssiThresholdFactor(...)
    .setPositionAveragingHistorySize(...)
    ...;
UseAdaptiveCalculation

Sets if adaptive calculation should be used. Adaptive Calculation adjusts several other configurations based on user movement. The movement is detected by using the accelerometer. (and the gyroscope, if available) The goal is to be more reactive while the user is moving and more stable while the user is standing still. The following config values are adjusted by adaptive calculation and all set values are therefore ignored: RssiEstimationHistorySize, BeaconTimeout, PositionCalculationInterval, PositionPublishInterval, UsePositionAveraging, PositionAveragingHistorySize

PositionCalculationInterval

Sets the frequency of position calculations. An interval of 1 means that after every scan update, a new position calculation is triggered. An interval of 2 means after every second scan update, and so on. Fewer position calculations save battery!

PositionPublishInterval

Sets the frequency of position updates received by the listeners. Does not affect how often the position is calculated!

RssiThreshold

Sets the signal strength threshold that determines at what RSSI value scanned beacons become relevant to the SDK. All scans with a lower RSSI than the threshold are ignored. In general, -100dB is a really weak signal, -50dB a strong and really close signal. This varies from device to device. This setting is only relevant if UseSensibleRssiFilter is set to false.

UseScoreLevelDetection

An advanced level-detection that considers several metrics in order to detect level-changes. Stabilizes the level in order to prevent unnecessary changes. There might be a higher delay until the level is actually changed, since the "confidence" needs to be higher. Works most efficiently if the device has an altimeter/barometer.

UseSensibleRssiFilter

Sets if the sensible RSSI filter should be used instead of the plain RSSI threshold. This applies a dynamic filter while determining the subset of beacons that shall be used for positioning. It determines the strongest received beacon RSSI and then only uses beacons that are weaker at maximum X * sensibleRssiThresholdFactor. It is a reasonable setting to make sure no unreasonably weak beacons are influencing the position-calculation. Recommended for most use cases.

SensibleRssiThresholdFactor

Sets the factor for the sensible RSSI filter. The factor can be any value between 1.1 and 1.5. With a factor of 1.2 and the strongest beacon having RSSI -70 dB, the weakest beacon could have -84 dB. This setting is only relevant if UseSensibleRssiFilter is set to true.

SensibleBeaconFilterDistance

As an addition to the SensibleRssiThresholdFactor this filter processes the ranged beacons to prevent the usage of beacons that are significantly farther away than the others. Specifically, away from the "weighted" centroid. Can be used if it is expected or observed that far away beacons are received strongly enough to pull the position away (e.g. on a large open space without beacons where beacons are received from the other side). Specify a distance after which beacons are to be filtered out. A recommended value is 10-15 meters, if problems are observed.

UseBeaconsFromAllLevels

The user’s position is calculated in the 2D space and gets enriched by a calculated level-number. Therefore, it might make sense to use beacons from all levels for the position-calculation. Otherwise the beacons are filtered and only the beacons are used from the level that was determined first. In general the strongest beacon(s) give(s) a good indication of the actual position, regardless of its level-number. So especially during level-changes this allows for a smoother transition. Most of the time there is no difference, but there are cases when it might be desired not to use beacons from all levels. At open areas where beacons from multiple floors can be ranged, this might lead to unwanted side-effects, e.g. the position will be displayed "in the air", hovering above a lower level.

NumberOfBestBeacons

Sets the maximum amount of beacons that should be used for positioning. If there is more scan data of beacons available, only the ones with the strongest RSSI signal strength will get used for the calculation.

UsePositionAveraging

Sets if position averaging should be used. Calculating a weighted average of past position updates helps stabilizing the position, but at the cost of an increased delay.

PositionAveragingHistorySize

Sets the amount of past positions that should be used for the position averaging. A higher value results in a more stable position, but it also increases the delay. This setting is only relevant if UsePositionAveraging is set to true.

UseCircularLateration

Sets if circular lateration should be used. This step uses the least squared algorithm to potentially improve the positioning results based on distance estimations. It happens at the cost of processing power.

CircularLaterationMaxIterations

Sets the maximum amount of iterations the circular lateration should perform. A higher value can result in improved accuracy, but at the cost of processing power. This setting is only relevant if setUseCircularLateration is set to true.

UseDistanceFilter

Sets if the distance filter should be used. This filter limits the distance a new position is allowed to be away from the previous one.

DistanceFilterMetersPerSecond

Sets how many meters per second the position is allowed to change if the distance filter is enabled. This setting is only relevant if UseDistanceFilter is set Path Snapping to true.

UseKalmanFilter

Sets if a kalman filter should be used. This filter can improve the positioning, at the cost of processing power.

Please make sure to stop the positioning before making changes to the configuration. This ensures that all settings are applied properly.

Navigation

The SDK offers the possibility to find the fastest routes using the navigation graph that was uploaded to the Backspin backend.

Building a NavigationTask

The first step to perform a navigation is to create an instance of NavigationTask using NavigationTask.Builder. When building the task it is required to add at least one destination.

NavigationTask task = BackspinSdk.Navigation.createBuilder()
      .addDestination(destinationLatLng, destinationFloor)
      .build();

In addition to addDestination(latLng, floor) you can also use addDestination(venue) or addDestination(venueLocation). If a venue location has multiple entrances the most convenient one will be chosen automatically.

It is possible to add multiple destinations to a single NavigationTask. By setting forceSequence() the SDK will respect the order in which the destinations were added. Otherwise all destinations get sorted by their line of sight distance to the start point.

NavigationTask task = BackspinSdk.Navigation.createBuilder()
      .addDestination(destinationLatLng1, destinationFloor1)
      .addDestination(destinationLatLng2, destinationFloor2)
      .addDestination(destinationLatLng3, destinationFloor3)
      .forceSequence()
      .build();

In order to get notified when one of the destination is reached, add a DestinationReachedListener when building the NavigationTask.

NavigationTask task = BackspinSdk.Navigation.createBuilder()
    .addDestination(destinationLatLng, destinationFloor)
    .setDestinationReachedListener(new DestinationReachedListener() {
            @Override
            public void onDestinationReached(Destination destination) {
                Log.i(TAG, "destination " + destination + " reached");
            }
        })
    .build();

Use setDestinationReachedDistance(meters) to adjust the distance at which the SDK sees a destination as reached.

There is also an NavigationFinishedListener which gets called when the user reaches the final destination.

...
    .setNavigationFinishedListener(new NavigationFinishedListener() {
            @Override
            public void onNavigationFinished() {
                Log.i(TAG, "navigation finished");
            }
        })
...

When the NavigationTask receives a position update the position gets snapped to the navigation route. Set the PositionSnappedListener to get a callback every time a snapped position is calculated. In most use cases, it makes sense to only display the snapped position on the map while a navigation is running.

...
    .setPositionSnappedListener(new PositionSnappedListener() {
            @Override
            public void onPositionSnapped(IndoorLocation location) {
                Log.i(TAG, "new snapped position");
            }
        })
...

If information about barrier freedom is included in the navigation graph data from the backend, you can use barrierFree() to build a NavigationTask that uses only barrier free paths.

Calculating a NavigationRoute

Once the NavigationTask is built, you can start calculating navigation routes. This can be achieved by calling task.calculate(startLocation, listener). A NavigationRoute with the given start location will then be calculated in a background thread.

task.calculate(startLocation, new RouteCalculationListener() {
    @Override
    public void onRouteCalculated(NavigationRoute navigationRoute) {
        Log.i(TAG, "route found! length in meters: " + navigationRoute.getTotalLength());
    }

    @Override
    public void onRouteNotFound() {
        Log.i(TAG, "route not found :/");
    }
});

The resulting NavigationRoute contains a list of navigation steps that describe the found route.

Use getWaypoints() to retrieve a list of IndoorLocation objects that can be connected by a polyline to draw the calculated path on a map.

Use getTotalLength() to get the total path length in meters and getEstimatedTravelTimeMillis() to return the estimated time left in milliseconds.

A recommended workflow for the navigation task is:

  • Create a new NavigationTask instance once the destinations are known.

  • Call task.calculate() on ever position update to update the navigation path.

  • Stop using task.calculate() after onNavigationFinished was called.

The first calculation on a NavigationTask needs more time to complete than all subsequent calls. Try to reuse the NavigationTask whenever possible. Do not create a new Task for each position update!

Turn by Turn Navigation

To give the user accurate navigation instructions, there is a NavigationDirection class. You can obtain it by calling route.getNextDirection(). It contains information about the next turn. The turn type (e.g. left) is stored as a String constant; they are static constants within the NavigationDirection class.

It is also possible to get a list of all directions to reach the target by calling route.getDirections(). If you only want a specific amount of directions you can use the route.getNextDirections(amount) method.

Path Snapping

If you want to snap an IndoorLocation to the navigation graph without the help of a NavigationTask, you can do this by calling the following method:

IndoorLocation snapped = BackspinSdk.Navigation.snapToPath(indoorLocation);

Configuration

Instead of configuring every NavigationTask individually it is also possible to set the defaults for all NavigationTask instances by using the NavigationConfig.

BackspinSdk.Navigation.getConfig()
    .setDestinationReachedDistanceMeters(...)
    .setDistanceDifferenceForRouteUpdateMeters(...)
    .setDistanceDifferenceForNewRouteCalculationMeters(...)
    .setOptimizeRoute(...)
    ...;
DestinationReachedDistanceMeters

Sets the default for the distance that has to be left on a route to declare a destination as reached.

DistanceDifferenceForRouteUpdateMeters

Sets the default distance difference in meters which needs to be exceeded in order for a new route update to be triggered.

DistanceDifferenceForNewRouteCalculationMeters

Sets the default distance difference in meters which needs to be exceeded in order for a new route calculation to be triggered.

OptimizeRoute

Sets if calculated routes should be optimized. Optimization tries to remove unnecessary nodes from the navigation route. This only works well if barriers exist.

Notification

The Backspin backend has its own customizable notification mechanism which can be setup via the dashboard. To receive callbacks for these events you have to start the notification module.

BackspinSdk.Notification.start();

If you are not interested in these notifications anymore you should call the corresponding stop method.

BackspinSdk.Notification.stop();
The notification module adds one ProximityWatcher for all beacon notifications and one ZoneWatcher for all venue and offer notifications. For more custom logic you can add additional ProximityWatcher and ZoneWatcher instances any time.

Listen for Notifications

To receive the notification callbacks while the module is started you have to add an NotificationListener.

BackspinSdk.Notification.addListener(new NotificationListener() {
    @Override
    public void onNotification(Notification notification, NotificationTrigger trigger) {
        Log.i(TAG, notification.getNotificationConfig().getTitle());
    }
});

The Notification class has the following properties:

LastNotificationTimestamp

The timestamp in milliseconds at which this notification was fired.

CurrentNumberOfNotifications

A counter which gets increased every time the notifcation gets fired.

NotificationConfig

This object contains all information which were entered in the Backspin WebFrontend like the title, message, triggers or associated beacons, venues and offers.

The callback also gives you a NotificationTrigger object which contains information about the beacon or zone which is responsible for the notification. Use trigger.getProximityEvent().getBeacon() if you are interested in the beacon which triggered the notification or trigger.getZoneEvent().getZone() for the corresponding zone.

Beacon Proximity

The SDK offers the possibility to raise events when the user enters or leaves a certain radius around a beacon. You can add ProximityWatcher instances to observe specific beacons. The Notification module adds one default watcher for all beacon notifications from the backend.

Create ProximityWatchers

By creating a ProximityWatcher you can declare specific events that should trigger actions when the device moves to a certain distance to a specified beacon.

ProximityWatcher watcher = BackspinSdk.Proximity.createWatcher()
    .addBeacons(beaconsOfInterest)
    .setListener(new OnProximityEventListener() {
        @Override
        public void onProximityEvent(ProximityEvent event) {
            Log.i(TAG, "proximity event: " + event);
        }
    });

The ProximityEvent object gives you the necessary details about the beacons of your interest you passed earlier. Calling getBeacon() returns the affected beacon, getDistance() gives you the range (immediate, near, far) and getType() tells you if you leave or enter this range.

Start & Stop ProximityWatchers

To connect the created ProximityWatcher to the current scanning of beacons you simply have to call its start() method. After that the corresponding OnProximityEventListener inside the ProximityWatcher called.

watcher.start();

If you no longer need the information about the beacons you were interested in you should call it’s stop() method.

watcher.stop();

Zone Proximity

Besides beacon proximity the SDK is able to observe specific areas as well. These areas are called zones and with the help of ZoneWatcher instances you are able to listen for enter and leave events. Furthermore you get continuous callbacks which tell you if you are inside or outside a zone. The Notification module adds one default watcher for all venue and offer notifications from the backend.

Create ZoneWatchers

To specify the zones you are interested in and what should be done you need to create a ZoneWatcher object.

ZoneWatcher watcher = BackspinSdk.Zone.createWatcher()
    .addZone(new Zone(latLngs, level))
    .setListener(new OnZoneEventListener() {
        @Override
        public void onZoneEvent(ZoneEvent event) {
            Log.i(TAG, "zone event: " + event);
        }
    });

The ZoneEvent instance gives you the necessary details about the zones of your interest you passed earlier. Calling getZone() returns the affected zone and getType() tells you if you leave/enter it or if you are inside or outside this zone.

Start & Stop ZoneWatchers

To connect the created ZoneWatcher to the SDK’s position calculation you simply have to call its start() method. After that the corresponding OnZoneEventListener gets called for each position update.

watcher.start();

If you no longer need the zone events you should call the stop() method.

watcher.stop();

Sensors

Backspin SDK contains several software sensors that use the devices hardware sensors to determine further information that could be interesting for an app with indoor positioning.

Altitude Sensor

The altitude sensor uses the device’s pressure sensor (if available) to measure the ambient air pressure and deliver the relative change in height. To get absolute height one needs the base pressure on sea level which differs with weather and place. Since the pressure difference changes predictably it is possible to use a simple formula to deliver altitude changes in meters.
With the relative altitude change floor changes can be estimated by dividing the relative altitude with the floor height. The floor height can be set by the Altitude Sensor user.
Finally you can ask whether a floor change is possible by calling isFloorChangePossible(). This looks at the pressure history to determine if there has been a change large enough. That feature is used when activating UseSensorFloorDetection in PositionConfig.

Movement Sensor

The movement sensor is used to determine if the device’s user is currently walking or not. In recent Android versions there is a step sensor available but our own implementation has two advantages: First, each device manufacturer implements the step sensor on their own. Our tests showed that these implementations react differently (in terms of sensitivity and accuracy) which leads to unexpected behaviors on differing devices. Second, the step detectors were implemented with a health tracking use case in mind. They act rather unsensitive to small movements. Backspin’s movement sensor is made to determine if a movement, that has been registered by the bluetooth positioning, is possible. In this use case it is favorable that the sensor reacts rather earlier than later.

Orientation Sensor

The orientation sensor relies either solely on the accelerometer and the magnetic compass or uses the gyroscope as well if it is available. The values returned are given as euler angles. There is also a smoothed yaw value which is very good for applications using a map that want to display the user’s orientation.

Even when a gyroscope is available, to determine yaw, the use of a compass is indispensable. Unfortunately its values are very inaccurate when used in a building. Use alternative sources whenever possible. (e.g. use the direction of a route when the user is currently navigating)

Usage

There are two ways of obtaining sensor data: Reading them whenever you need the most recently one or getting notified when new data is available. Let’s say you want to read user movement and get notified for orientation changes.

SensorReader<MovementSensor> movementReader = new SensorReader<>();
MovementSensor movementSensor = BackspinSdk.Sensor.registerReader(movementReader);

//now you can read the sensor data
movementSensor.isUserMoving();

BackspinSdk.Sensor.unregisterReader(movementReader);

SensorObserver<OrientationSensor> orientationObserver = new SensorObserver<>(){
    public void notifySensorUpdate(OrientationSensor sensor){
      //this gets called upon every sensor update
      sensor.getSmoothedYaw();
    }
};

//When you do not need the sensor anymore:
BackspinSdk.Sensor.unregisterObserver(orientationObserver);

After finishing using the sensors simply unregister them. You do not have to worry about other observers: The sensor only stops working when no one is observing or reading them.

Do not forget to unregister your observers and readers afterwards. Otherwise the sensors go on to retrieve data and use energy!

Sensor Configuration

With BackspinSdk.Sensor.getConfig you can obtain the SensorConfig object to adjust certain parameters. The following table should help you doing so:

What to do if…​

Normal usage of the phone is recognized as movement?
  • MovementMaxMsTimeBetweenSignumChange higher

  • MovementThreshold higher

The movement estimation is too sensitive?
  • MovementThreshold higher

  • MovementNumberOfCorrectPeaksRequired higher

  • MovementNumberOfCorrectPeaksRequired / MovementPeakFactor higher

The floor estimation is not accurate?
  • set AltitudeFloorHeight to the correct value in meters

The absolute altitude is not accurate?
  • set the correct AltitudePressureSeaLevel (changes very often)

You can look up the default values in the SensorConfig class. (e.g. SensorConfig.DEFAULT_MOVEMENT_THRESHOLD)

Analytics

Backspin provides different statistics, analytics and heatmaps to gather information about its clients. The only thing it needs is plentiful data in form of AnalyticsEvent objects.

An event always contains the following fields:

Field Name

Example value

type

"THE_TYPE"

timestamp

1441283129969

timezone

"Europe/Berlin"

timeoffset

"7200"

device

"LG Nexus 5X"

platform

"android"

platformVersion

"6.0.1"

Additionally all the event information is sent, including the root scope id and (hashed) app and user account IDs.

Enqueue Events

The SDK offers a simple API to create and send events.

BackspinSdk.Analytics.createEvent("myCustomEventType").enqueue();

You can also adjust several settings and add additional content via a custom payload which is simply a collection of key-value-pairs.

BackspinSdk.Analytics
     .createEvent("myCustomEventType")
     .addPayload("custom_info_int", 123)
     .addPayload("custom_info_string", "more info")
     .setIndoorLocation(indoorLocation)
     .enqueue();
All enqueued events are gathered in the underlying database and will be sent in a big batch after approximately 20 minutes.

Enable or Disable Events

With the help of the AnalyticsConfig you can define which types of events should be sent or not. Let’s say you want to enable analytics.

BackspinSdk.Analytics.getConfig().enable();

To disable them again you simply have to call:

BackspinSdk.Analytics.getConfig().disable();

If you want to enable or disable specific event types only, you can do that as well. For example, if analytics is enabled, the SDK already enqueues a new event of type AnalyticsEvent.TYPE_LOCATION_UPDATE on every position update. To disable it, simply call:

BackspinSdk.Analytics.getConfig().disable(AnalyticsEvent.TYPE_LOCATION_UPDATE);
The whole Analytics module is disabled by default.

Sending Events to the Server

All enqueued events will be sent to the server after a certain time. By default this time is set to 20 minutes and it can be adjusted with the following method:

BackspinSdk.Analytics.getConfig().setSendInterval(60000L);
The actual time of the event transmission to the backend can differ because it depends on various factors like the sleep/doze mode of the device for example.
Decreasing the interval means sending http requests to the server more often which leads to more battery consumption.

Default Events

If you have enabled the module analytics the SDK automatically enqueues to following default event types.

TYPE_LOCATION_UPDATE

Will be enqueued on every position update. The internal key is location_updated.

TYPE_NOTIFICATION_TRIGGERED

Will be enqueued whenever a notification is fired. The internal key is location_triggered.

Asset Tracking

The module of the following Chapter was deprecated and replaced by the new subscriptions module.

This module provides information about the tracking of objects which are assigned to a specific user.

All actions concerning this module required an active favendo user token. Set it in the ConnectionConfig or with the BackspinSdk.Assets.setUserToken method when initializing the SDK if you want to use any of the asset tracking API methods.

There are the following new models available:

Zone

An area which is represented through a polygon.

Asset

Represents an object of interest which is tracked via bluetooth, wifi or another connection.

AssetPosition

A geo location of a specific asset.

ZoneAlarm

This is a general definition about which assets should be watched concerning specific zones.

ZoneAlerts

An alerts is the specific object which is created whenever an asset enters or leaves a watched zone. You can see if it was entered or left with the help of the ZoneAlert.getTrigger() method. It could be either ZoneAlert.ENTER or ZoneAlert.EXIT.

The public BackspinSdk.Assets class offers the following load methods to start asynchronous calls to the backend. Each method accepts an generic listener which gives you the according results after a successful request. All of these results depend on the UserToken you set earlier.

loadAssets

Load all assets of the current user.

loadAssetsByExternalId

Load assets by their externalId.

loadPositions

Loads the last known positions of the users assets. There are also overloads for requesting only positions of specific assets.

loadZoneAlarms

Loads the users zone alarms which have been created earlier. There are also overloads for requesting only alarms of specific assets.

loadZoneAlerts

Loads the last triggered zone alarms from the backend.

After a successful call the results are stored in the local database and can be get via the following methods:

getAssets

Gets all previously loaded assets.

getAssetByExternalId

Gets previously loaded assets by their externalId.

getZones

Gets all loaded zones.

getAssetPositions

Gets the last requested asset positons.

getZoneAlarms

Gets all personal alarms for specific zones.

getZoneAlerts

Gets the last triggered alarms.

AssetWatcher

If you want continuous updates about the status of your assets you can use the AssetWatcher class. Use the addAsset method to specify which assets should be observed. There is also a removeAsset method that can be called on the watcher if the asset is no longer of interest.

If you don’t add any assets the system will observe all of the users assets per default.

Calling the watchers start() method will start the actual observation process. Use the stop method if you are not interested in these updates anymore.

You can set an AssetPositionUpdateListener and an AssetAlertListener. The first one gets called whenever the position of an observed asset changes and the second whenever an asset enters or leaves a specific zone.

In addition there is a ConnectionListener that receives updates on the connection of the watcher to the backend.

A complete code example would look like this:

AssetWatcher watcher = BackspinSdk.Assets.createWatcher()
  .addAsset("assetId1")
  .addAsset("assetId2")
  .setPositionUpdateListener(new AssetPositionUpdateListener() {
      @Override
      public void onPositionUpdate(AssetPosition position) {
          Log.i(TAG, String.format("asset %s has new position %s", position.getAsset().getId(), position.getIndoorLocation()));
      }
  })
  .setAlertListener(new AssetAlertListener() {
      @Override
      public void onAlert(ZoneAlert alert) {
          Log.i(TAG, String.format("asset %s appeared in %s zones", alert.getAsset().getId(), alert.getZones().size()));
      }
  })
  .start();

Subscriptions

Using the Subscriptions API you can choose to be notified of position updates for tracked users, which can then be used in conjunction with our Favendo Map SDK.

It is not recommended to use the AssetTracking and event subscription methods side by side. It may result in unexpected behaviours.

Following is a basic invocation of one of the Subscription API methods.

Subscription subscription = BackspinSdk.Events.subscribe(eventClass, eventListener);

The subscribe method returns an instance of interface type Subscription which you could then use to call the close method when you no longer want to receive updates for that specific subscription as depicted below.

subscription.close();

Alternatively, you can also use the static closeAll method on BackspinSdk.Events in order to stop receiving updates for all of your active subscriptions at any point in time as depicted below.

BackspinSdk.Events.closeAll();

Upon using the Subscriptions API, Backspin SDK triggers the following types of events, which would be of interest to you.

In order to use Subscriptions API, ensure that you call BackspinSdk.setUser method first, which ensures that an appropriate token is available to use the subscription feature.

ConnectionStateEvent

Subscriptions establish a two way connection to the Backspin backend to transfer the data. If you are interested in connection state changes on that underlying connection you can register for this event and see if the connection is active or not. The EventScope parameter is ignored for this event type. You should use subscribe overload which does not include the EventScope as its parameter and pass on the appropriate event type as depicted below. Upon successful connection establishment you should expect one instance of this event.

Subscription subscription = BackspinSdk.Events.subscribe(
    ConnectionStateEvent.class,
    new EventListener<ConnectionStateEvent>() {
        @Override
        public void onEventReceived(ConnectionStateEvent event) {
            Log.i(TAG, "isConnected: " + event.isConnected());
        }
    });

TrackingEvent

It provides the positioning information on a subject being tracked. This event could be categorised into the following subcategories with respect to the availability of the position of the subject, which could be an asset or another user.

  • TrackingEvent.TRACKING_STATE_ACTIVE
    Presents a tracking event type carrying the most recent positioning information about a subject for which the given user has the necessary permission granted in order to receive the location information. This event is always triggered with the live position of the subject.

  • TrackingEvent.TRACKING_STATE_NOT_AVAILABLE
    Presents a tracking event type carrying the other relevant information about a subject apart from the ones related to positioning. This behaviour comes from the fact that the given user does not have necessary permission to have the location information about the subject. It can happen due to the adaptation in visibility setting or deletion of the subject.

  • TrackingEvent.TRACKING_STATE_OUTDATED
    Presents a tracking event type carrying the last known location information about the given subject. This event is triggered whenever the app establishes a new WebSocket connection to the Backspin server and there is a recorded last known position for the subject, provided that the given user also has permission to receive the position information for it.

Upon first subscription attempt, one of following three possible scenarios is expected.

  • If the permission to receive subject’s location information is granted and the last know location is available for the subject, the same will be presented contained inside one TRACKING_STATE_OUTDATED event, followed by TRACKING_STATE_ACTIVE events with live positioning information as expected.

  • If the permission to receive subject’s location information is granted and the last know location is unavailable for the subject, live positioning information would directly start coming in contained inside TRACKING_STATE_ACTIVE events.

  • If the permission to receive any subject’s location information is not granted and subscription attempt is made for that specific subject, exactly one TRACKING_STATE_NOT_AVAILABLE event should come in. In case the subscription attempt is made using EventScopes.all convenience method, no event should come in. While receiving TRACKING_STATE_ACTIVE events for the subscribed subject(s) if the permission to receive the location information is denied, application users would receive exactly one TRACKING_STATE_NOT_AVAILABLE event.

In the reverse scenario, while receiving the the TRACKING_STATE_NOT_AVAILABLE events for the subscribed subject(s) if the permission to receive the positioning information is granted, application users would start receiving the TRACKING_STATE_ACTIVE events for the subject(s).

In case of any disruption in the WebSocket connectivity while consuming the service the events would stop coming in. Upon reconnection users should receive an incoming ConnectionStateEvent followed by one of the three possible scenarios described in the case of first subscription attempt as applicable.

This event contains the following general information:

trackable

Contains the public id and a descriptor of the trackable subjects.

position

Contains the position of the subject. The position information could be null in case of TRACKING_STATE_NOT_AVAILABLE category of events.

timestamp

Contains the timestamp when the event is generated.

trackingState

Indicates the state with respect to the availability of the positioning information for the subject. This information always corresponds to the subcategories of this event type which were described above, namely TRACKING_STATE_ACTIVE, TRACKING_STATE_NOT_AVAILABLE and TRACKING_STATE_OUTDATED. For instance when the state is TRACKING_STATE_NOT_AVAILABLE, due to the change in visibility permission or deletion of the subject, the position information will be null.

intersectedVenues

Contains a collection of LoadableObjectReference which hold references to venues in which this position update lies. Use the load method to get the respective Venue instance out of the local database. If no venue can be found this method call will return null. Initialize the SDK first and make sure you loaded all venues from the backend.

Subscription subscription = BackspinSdk.Events.subscribe(
    TrackingEvent.class,
    EventScopes.user("12345"),
    new EventListener<TrackingEvent>() {
        @Override
        public void onEventReceived(TrackingEvent event) {
            Log.i("SUBS", "id: " + event.getTrackable().getPublicId() + " position:" + event.getPosition());
        }
    });

The code snippet above depicts the basic use of Subscriptions API to get position updates for a subject. In this specific example, we are interested in position updates about a subject having the id 12345 and this represents our present scope. However, you could also use the EventScopes.all convenience method to receive the position updates for all of your visible subjects.

In order to ensure an efficient use of mobile data and device memory it is recommended to close all subscriptions when the application user is away from the view, which uses the Subscriptions API or if the application is not in foreground.