App to App Library (ScanLib)
The Bettermile Driver App can be used (next to it running on a secondary device) also on a one device (meaning on one device together with other Apps - e.g. a loading, delivering or scanner App). In order to allow a seamless experience for the drivers - as they are in need to go back and forth, especially throughout the day when delivering and navigating - the offered App to App library supports the following use cases:
- Let the Bettermile Driver App open your app - recommended
- Open a specific page of the Bettermile Driver App (auto-switch) - recommended
- Sending the tour setup (a.k.a Quick Login feature)
- Sending the scan results
Below you'll find the technical details:
How to integrate the Android ScanLib
Using the ScanLib Android Library the Bettermile Driver Android App can query, open, and ask for scan results to update a tour’s data from a 3rd party scanner app without knowing its package name. Also, the scanner app can respond to a scan request or ask for opening a specific page of the Bettermile Driver App.
How ScanLib works
ScanLib is a simple library that wraps all the communications between two apps behind a static object called ScanLib to make the integration easier for the scanner app developers.
General Note
For historic reasons sesc
(or second screen) is refering to the Bettermile Driver App in the code.
The Scanner app implementation
The scanner app needs to add the ScanLib GitHub Packages repository to it’s build.gradle
:
repositories {
google()
mavenCentral()
...
maven {
name = "GLS ScanLib"
setUrl("https://maven.pkg.github.com/bettermile/sesc-scan-lib")
credentials {
username = System.getenv("GLS_SCANLIB_USER")
password = System.getenv("GLS_SCANLIB_TOKEN")
}
}
...
}
and define ScanLib
as a dependency:
implementation("com.gls.scanlib:scanlib:1.0.0-beta.10")
If by any reason you are not using the gradle build system and would like to download the AAR directly, you can use this link and your credentials:
https://maven.pkg.github.com/bettermile/sesc-scan-lib/com/gls/scanlib/scanlib/1.0.0-beta.10/scanlib-1.0.0-beta.10.aar
Notes
- We will provide you
GLS_SCANLIB_USER
andGLS_SCANLIB_TOKEN
. Please keep them somewhere safe out of the code Git repository. It can be stored somewhere like your development Local machine environment variables and your CI environment variables like GitHub Action Secrets. - The scanner app is responsible to persist the current user data after login and a proper UX after being called from the Bettermile Driver App.
- Once the user wants to open a scanner app from the Bettermile driver app, we would recommend that the scanner app opens right away the screen where the user can scan parcels to keep the user flow as smooth as possible.
Let the Bettermile Driver App open your app
If the Bettermile Driver App should be able to open your scanner app, you should add this block to your AndroidManifest.xml
. The intent-filter
block should be part of the activity block
, which should be accessible from the Bettermile Driver App. The Bettermile Driver App can then query the activity
via the Android OS. The activity
will be requested for an app launch or to request for a parcel scan:
<intent-filter>
<action android:name="com.gls.secondscreen.action.SCAN" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
Sending the scan results
The Bettermile Driver App starts the scanner app using startActivityForResult
and there are a solution for passing scan result to the Bettermile Driver App by using Broadcast messages
Using Broadcast messages

While the scanner app is still open it can transfer scan results back to the Bettermile Driver App ignoring if the scanner app has been opened through the Bettermile Driver App or opened directly. The ScanLib.sendScanResult
makes it possible:
ScanLib.sendScanResult(
activity = this,
scanResult = GlsModelScanResult(parcels = scannedParcels)
)
This solution uses broadcast messages in the background. The Bettermile Driver App should already have been running in the background and the user has to be logged in and set up the tour. If the Bettermile Driver App is running, it shows proper toasts from its scan service. In this case, the Scanner app can close itself without passing further results by calling Activity’s finish()
method if it was opened from the Bettermile Driver App for a result or Launch the Bettermile Driver App by calling one of the LaunchModes. The scanLib has a callback function that Scanner app should register to it on Start and wait for the callback containing Waypoints that are done as outcome of scan results:
override fun onStart() {
super.onStart()
ScanLib.startWaypointChangedListener(this) { doneWaypoints ->
// show a dialog to user to get back to the Bettermile app
}
}
This can be done during Activity’s onStart
lifecycle method.
The scanner app should also stop the callback when it has no ui and the app has moved to background to avoid any leaks:
override fun onStop() {
super.onStop()
ScanLib.stopWaypointChangedListener(this)
}
This can be done during Activity’s onStop lifecycle method. The dialog can be something like this from sample app:

Open a specific page of the Bettermile Driver App
The scanner app can open different screens of the Bettermile Driver App using the scanLib. This block launches the app without opening any special pages:
if (ScanLib.launchApp(this, GlsModelLaunchMode.Launch)) {
finish()
} else {
Toast.makeText(this, "The Bettermile app is not installed!", Toast.LENGTH_LONG).show()
}
This block opens the navigation page in different modes:
if (ScanLib.launchApp(
this,
GlsModelLaunchMode.Navigation(
autoStart = true, // Auto start navigation after landing on navigation page
split = true, // Open navigation page in split mode showing waypoints list
)
)
) {
finish()
} else {
Toast.makeText(this, "The Bettermile app is not installed!", Toast.LENGTH_LONG).show()
}
Note The Bettermile Driver App takes care of user login and tour setup before opening any of those pages.
Sending the tour setup (a.k.a Quick Login feature)
Scanner app can define the user and force it including the tour data on the SeSc app for that day session. The ScanLib.setupTour
method is so general for different customer usage purposes, but this way it works proven for one active customer:
val tour = GlsModelTourSetup(
ScanLib.Constants.TENANT_ID_COUNTRY,
tourNumber,
listOf(
GlsModelTourSetupUniqueAssignment(
tourNumber,
scannerIds
).toJsonString()
)
)
setupTour(username, forceUsername, tour)
By forcing the username the user cannot change it and only can enter the password. You may also ignore sending the tour data, but if you set it then the user will jump from the tour setup step and get directly into the app. If the SeSc app receives a forced user which is different from the currently logged in user, it will logout the user and wait for a new login. This tour setup data will be deleted at midnight and the scanner app should set the data the next day for the new tour (or the new user!)
Note You can leave the assignment list empty if you don’t have access to this data or want to let the driver decide.
The Sample Scanner app
The ScanLib comes with decent documentation inside the code and also a sample Android app that demonstrates all these features.
To clone this repo you can use the provided token:
git clone https://${GLS_SCANLIB_TOKEN}@github.com/bettermile/sesc-scan-lib-sample.git --branch=main
To run the app you also need to replace the hard-coded credentials inside the app/build.gradle.kts
with your:
credentials {
username = System.getenv("GLS_SCANLIB_USER")
password = System.getenv("GLS_SCANLIB_TOKEN")
}

Scan results data structures
GlsModelScanResult
/**
* A parcelable data model of Scan result
*/
data class GlsModelScanResult(
/**
* The list of [GlsModelScanParcel] containing scanned parcels
*/
val parcels: List<GlsModelScanParcel>,
)
GlsModelScanParcel
/**
* A parcelable data model of scanned parcels
*/
data class GlsModelScanParcel(
/**
* Parcel/Pickup server ID
*/
val parcelId: String,
/**
* Parcel Scan event time as Unix time.
* Default value: [System.currentTimeMillis]
*/
val scanTime: Long = System.currentTimeMillis(),
/**
* Parcel Scan status as [GlsModelScanStatus].
* Default value: [GlsModelScanStatus.PROCESSED]
*/
val scanStatus: GlsModelScanStatus = GlsModelScanStatus.PROCESSED,
/**
* Parcel/Pickup was delivered/collected or not [GlsModelScanOutcome].
* Default value: [GlsModelScanOutcome.UNKNOWN]
*/
val scanOutcome: GlsModelScanOutcome = GlsModelScanOutcome.UNKNOWN,
/**
* A short description of the scan event to be displayed in the tour analysis.
* For example: "Parcel lost in a river!"
*/
val scanDetails: String = "",
)
GlsModelScanStatus
enum class GlsModelScanStatus {
/**
* The job was finished by the driver, either successfully or unsuccessfully
*/
PROCESSED,
/**
* The previous [PROCESSED] state got reverted.
* For example: the parcel is again out for delivery
*/
UNPROCESSED
}
GlsModelScanOutcome
/**
* The parcel/pickup delivery outcome
*/
enum class GlsModelScanOutcome {
/**
* The parcel/pickup delivery is not known
*/
UNKNOWN,
/**
* The parcel/pickup delivered successfully
*/
SUCCESSFUL,
/**
* The parcel/pickup delivered unsuccessfully
*/
UNSUCCESSFUL,
}
GlsModelWaypointResult
/**
* A parcelable data model of Waypoint result
*/
data class GlsModelWaypointResult(
/**
* The list of [GlsModelWaypoint] containing done waypoints
*/
val doneWaypoints: List<GlsModelWaypoint>,
)
GlsModelWaypoint
/**
* A parcelable data model of waypoint
*/
data class GlsModelWaypoint(
/**
* Waypoint server ID
*/
val waypointId: String,
/**
* Waypoint address street
*/
val street: String? = null,
/**
* Waypoint address street number
*/
val streetNumber: String? = null,
/**
* Waypoint address postcode
*/
val postcode: String? = null,
/**
* Waypoint address city
*/
val city: String? = null,
)
Please reach out to your respective Sales or Customer representative for access as well as more information if needed.