Setup
To use Backspin in your Android application you have to add our Maven repository
to your projects build.gradle
file.
allprojects {
repositories {
maven {
url "${favendo_artifactory_url}/backspin-android-sdk"
credentials {
username = "${favendo_artifactory_user}"
password = "${favendo_artifactory_password}"
}
name = "plugins-favendo"
}
}
}
The minimum API level of the SDK is 15. But in order to use any of the BLE related features you need at least API level 18. |
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.
favendo_artifactory_user = your_name
favendo_artifactory_password = your_password
favendo_artifactory_url = http://maven-repository.favendo.de/artifactory
Now you have to add the dependency to your module’s build.gradle
file.
dependencies {
implementation 'com.favendo.android:backspin-sdk:3.20.13'
}
The Backspin SDK requires several permissions to operate. Your app will not
work unless you add the following lines to your AndroidManifest.xml
file:
<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" />
Android 5.0 and above requires runtime permissions for
ACCESS_FINE_LOCATION . 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
.setUserToken(userToken); // 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));
}
});
You can control the SDKs log output by calling BackspinSdk.setLoggingLevel() .
It is also possible to disable the logging completely via BackspinSdk.disableLogging() .
|
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. Loading data
is restrained when the time interval specified in ConnectionConfig
since the
last load has not passed yet. 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.
// receive a list of all beacons from the current scope
List<Beacon> beacons = BackspinSdk.Data.getBeacons();
// receive 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 neither have
their VenueOpeningTime
nor VenueLocation
information loaded you can use
the excludePropertiesFromVenues()
method.
ConnectionConfig connectionConfig = new ConnectionConfig(authKey, urlType)
.excludePropertiesFromVenues(Venue.OpeningTimes, 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 15 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 this on pre-lollipop (Android < 5) 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 4.x devices (only the devices which have TLS 1.2 support)
Tls12SocketFactory.enableTls12OnPreLollipop(builder);
glide.register(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory(builder.build()));
}
}
Account Data
There are three entities that are bound to a certain user account ID which is assigned when initializing the SDK for the first time after a fresh install on a device:
-
Likes
-
Leaflet
-
AccountProfile
By default, all account bound data will be synchronized with the backend during
BackspinSdk.load()
.
If you have no need for account bound data you can simply
deactivate them by calling the according synchronize…()
method
on the ConnectionConfig
instance.
This results in a faster BackspinSdk.load()
.
The following snippet will deactivate all account data synchronization:
ConnectionConfig connectionConfig = new ConnectionConfig(authKey, urlType)
.synchronizeAccountProfile(false)
.synchronizeLikes(false)
.synchronizeOfferleaflet(false);
Likes
Every user has its own collection of liked objects. You can like every object
which implements the Likeable
interface, Venue
and VenueOffer
for example. You can manipulate the likes with the help of a LikeEditor
:
LikeEditor editor = BackspinSdk.Data.createLikeEditor();
An LikeEditor
provides a like()
and a dislike()
method,
some getters to check the current state and a synchronize()
method
which is used to synchronize the state of the current LikeEditor
with
the backend. There is also an contains()
method which can be used
to check if an specific object was already liked.
When calling synchronize() your likes are stored in
the local database. So if the server request fails because of no or bad
internet connection the system will still know what likes you have setup.
Whenever the SDK is trying to load or reload it’s data
it will also retry to upload these changes to the backend.
|
A complete example looks like this:
BackspinSdk.Data.createLikeEditor()
.like(venue1)
.like(venueOffer1)
.dislike(venue2)
.dislike(venueOffer2)
.synchronize();
You should take advantage of android’s lifecycle methods.
For example use the activity’s onStart() method to create the LikeEditor
and onStop() to synchronize it’s current state.
|
Leaflet
Every user can save his favorite venue offers on his leaflet.
You can manipulate the user’s leaflet with the help of a LeafletEditor
:
LeafletEditor editor = BackspinSdk.Data.createLeafletEditor();
An LeafletEditor
provides an add()
and a remove()
method,
some getters to check the current state and a synchronize()
method
which is used to synchronize the state of the current LeafletEditor
with
the backend. There is also an contains()
method which can be used
to check if an specific object is already on the leaflet.
When calling synchronize() your leaflet is stored in
the local database. So if the server request fails because of no or bad
internet connection the system will still know what venue offers you have
on your leaflet. the SDK is trying to load or reload it’s data
it will also retry to upload these changes to the backend.
|
A complete example looks like this:
BackspinSdk.Data.createLeafletEditor()
.add(venueOffer1)
.remove(venueOffer2)
.synchronize();
You should take advantage of android’s lifecycle methods.
For example use the activity’s onStart() method to create the LeafletEditor
and onStop() to synchronize it’s current state.
|
AccountProfile
To edit a profile and make UI generation more convenient you can use the AccountEditor
.
It contains a list of AccountField
that can have differing types:
-
Single choice out of a list of items
-
Multiple choices out of a list of items
-
Range choice (from / to) out of a list of items
-
A single free text value
-
A list of free text values
-
A range (from / to) as two free text values
The class AccountInputItem
represents an item the user can choose. FreeTextValues
are simply strings, but have four different types:
-
Text
-
Integers
-
Email Adresses
-
Dates
There is no check wether the input is correct, but you should at least provide
a matching software keyboard. Each free text field can give the correct Android integer flag
InputType
to assign it to the corresponding EditText
.
To create a list of fields with their current entries the code would look like this:
List<AccountField> fields = accountEditor.getAccountFields();
// List item creation:
TextView tvTitle;
tvTitle.setText(fields.get(i).getTitle());
TextView tvContent;
tvContent.setText(fields.get(i).getContent());
Content of a field is a automatically generated string showing what the user has chosen before or an empty string if nothing has been assigned yet.
If a user wants to edit a certain field you need the correct subclass of AccountFields
to be able to manipulate its data. The code to achieve this looks like this:
accountEditor.editField(fields.get(i), this);
Assuming that the calling class implements the AccountFieldEditor
interface.
It contains a method stub for each AccountField
type and the right one is called
by the AccountEditor
. There you could for example start a dialog.
// part of AccountFieldEditor implementation:
void onSingleChoice(AccountFieldSingleChoice field) {
AccountInputItem item = field.getInputItems().get(0);
field.setField(item);
}
void onFreeTextSingle(AccountFieldFreeTextSingle field) {
field.setValue("Free Text Value");
}
Don’t forget to update the profile in the SDK later on. E.g. after
leaving the activity where the profile could be edited by the user.
This can be done by calling synchronize
in the AccountEditor
.
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:
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()
.
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()
afteronNavigationFinished
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? |
|
The movement estimation is too sensitive? |
|
The floor estimation is not accurate? |
|
The absolute altitude is not accurate? |
|
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 |
TYPE_NOTIFICATION_TRIGGERED
|
Will be enqueued whenever a notification is fired.
The internal key is |
Asset Tracking
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 via setUserToken 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
|
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();