Native Android access SDK in depth explanation
Native Android access SDK in depth explanation
This article will guide you step by step on how you can integrate the spintly access sdk into your app. Spintly do provide a sample app and sample app source code on how the app can integrate with the spintly access sdk, if you havent got the app please contact Spintly for the same, as the sample app will help you understand things faster. For this SDK integation to happen you need to either do the type2 or type3 integration.
Once the client is done integrating the sdk in thier app, the app needs to be sent to spintly for review, this review is needed so that the sdk is properly used in the client app in accordance to spintly standard, Once spintly approves the app, the client can go ahead and release the app on playstore and appstore.
Please note you cannot integrate spintly access sdk standalone, it will always go hand in hand with the api
1 Changes required in the client app to support Spintly SDK
There are certain steps we need to follow at different stages of the app flow. Following are the locations where sdk-related changes need to be added.
1. During app initialization
2. After login
3. Mandatory checks
4. Getting a list of doors
5. Starting Bluetooth scan
6. Opening doors
7. On notification
8. Logout
2 SDK Details
Spintly integration requires the following SDKs to be used by the client app:
- OAuth SDK: This SDK is used for getting a Spintly access token in exchange for the user's client jwt token.
- Access control SDK: This SDK contains the actual access control functionality
3 High-Level Overview
Spintly uses Auth 2.0 Token Exchange, for more information please visit this site:
https://datatracker.ietf.org/doc/html/rfc8693
Here in the diagram you can see the JWT token is given to the OAuth SDK and in return, the OAuth SDK returns the access token. This access token is then passed to the Spintly access control SDK.
4 During App Initialization: OAuth SDK
Before using the SDK, the app developer needs to specify the clientId, providerId and environment. The clientId and providerId will be provided by the Spintly Support person to the app developer. The environment will be specified by the developer based on whether the developer is building the app for production or a sandbox environment. Different clientId and providerId will be provided for the production and sandbox environment.
Context context = getApplicationContext();
String clientId = "clientIdString";
String provider = "providerString";
String environment = SpintlyOauthManager.PRODUCTION; //or
SpintlyOauthManager.SANDBOX
SpintlyOauthManager spintlyOauthManager = new SpintlyOauthManager(context, clientId, provider, environment);
5 During App Initialization: Access Control SDK
Just like the OAuth SDK on the previous page, the environment needs to be set for the access control SDK after its instance is initialized.
// Initialize instance of access SDK
SpintlyACServiceProvider
serviceProvider = SpintlyACServiceProvider.getDefaultInstance(getApplicationContext());
// set environment
String environment = EnvironmentManager.PRODUCTION // or
EnvironmentManager.SANDBOX;
EnvironmentManager environmentManager = serviceProvider.getEnvironmentManager(environment)
environmentManager.setEnvironment(environment);
//The set function will throw error if unknown environment is passed
// If you want to know the current environment use the below:
String currentEnvironment = environmentManager.getEnvironment();
6 After login: OAuth SDK
The user's client jwt token which was obtained after the user logged in to the app, needs to be passed to the OAuth SDK. The OAuth SDK in return will give back a Spintly access token.
Note: Here the user's client JWT token contains a unique Id that identifies a particular user, this unique Id must be added to Spintly using the Spintly backend APIs.
Authorization callback = new AuthorizationCallback() {
@Override
public void onSuccess(SpintlyOauthSession session) {
String accessToken = session.accessToken.getJWTToken();
//To be passed to other Spintly services that make use of this token
}
@Override
public void getAuthenticationDetails (AuthenticationContinuation continuation){
// this is called when user is logged out or all tokens have been expired and
// need login parameters again from the client.
//Note: This is where the client app’s clientToken is used
// For Token Exchange grant type authorization:
AuthenticationDetails authenticationDetails =
AuthenticationDetails.createWithTokenExchangeGrantType(clientToken);
continuation.setAuthenticationDetails(authenticationDetails)
continuation.continueTask();
// If clientToken was not available when this function was called, hold on to the continuation reference and once clientToken is acquired, repeat the above steps.
// Or call getSession again and repeat the process
}
@Override
public void onFailure (Exception exception){
// Any error including network errors will come here;
}
};
spintlyOauthManager.createSession(callback);
//This will create new token on every call
7 After login: Access Control SDK
The Spintly access token that is generated by the OAuth SDK is then passed to the Spintly access control SDK. After this step succeeds, functionalities of the access SDK can be used.
//Supports only one credential at this moment
CredentialManager credentialManager =
serviceProvider.getCredentialManager();
credentialManager.logIn(accessToken, new CompletionCallback<Void>() {
@Override
public void completed(Void result) {
//login success
}
@Override
public void failed(Throwable exc) {
//login failure
}
});
//Calling above function will first logout existing credential even on login failure
// Note: A token refresh handler must be registered prior to logging in any user.
// This should be added in application onCreate itself.
Note: The Spintly access token will have an expiry. Client is responsible to update Spintly access token on the event that it is expired. The refreshAuthentication() trigger means that the access SDKs token has expired. When this trigger is received:
- The AuthorizationCallback() of the OAuth SDK that was used earlier can be used.
- A fresh clientToken needs to be passed to the getAuthenticationDetails() of the AuthorizationCallback.
- spintlyOauthManager.getOrCreateSession(callback); can be used instead of the createSession()
- Upon receiving the Spintly access token in onSuccess() of AuthorizationCallback use it in the next step.
- With the new access token client app completes the refreshAuthentication() function’s callback i.e. completionCallback.complete(newAccessTokenString);
- If the AuthorizationCallback gave a failure, still client app should complete this function’s callback i.e. completionCallback.failed(error);
An event listener can be attached in the following way. As mentioned, this event is sent whenever an API call fails due to access token expiry.
//In application onCreate()
CredentialManager credentialManager =
serviceProvider.getCredentialManager();
credentialManager.setTokenRefreshHandler(new TokenRefreshHandler() {
@Override
public void refreshAuthentication(@NonNull CompletionCallback<String> completionCallback) {
//This trigger means that the access SDKs token has expired
// When this trigger is received:
//1. The AuthorizationCallback() of the OAuth SDK that was used earlier can be used.
//2. A fresh clientToken needs to be passed to the getAuthenticationDetails() of the AuthorizationCallback.
//3. spintlyOauthManager.getOrCreateSession(callback); can be used instead of the createSession()
//4. Upon receiving the Spintly access token in onSuccess() of AuthorizationCallback use it in the next step.
//5. With the new access token complete this functions callback i.e. completionCallback.complete(newAccessTokenString);
//5. If the AuthorizationCallback gave a failure, still complete this functions callback i.e. completionCallback.failed(error);
}
});
We have noticed that the SDK login can fail due to server or network errors. In this case the app is responsible to retry login to the SDK for the following cases.
- Check on every app launch
- Schedule a job that fires on internet access availability
- Check on permission update notifications (if implemented)
Please check if the user is already logged in the app first before calling the SDK login. This is important as the login process first performs an internal logout which can cause a logged-in user to get logged out if there are network issues. To checked is a user is logged-in, use the below:
//Checks if the access SDK is logged in
boolean isAccessSdkLoggedIn = credentialManager.isLoggedIn();
8 Getting list of access points
This snippet of the code explains how to get all the devices to which a particular user has permission to.
PermissionManager permissionManager =
serviceProvider.getPermissionManager();
List<AccessPoint> accessPoints = permissionManager.getAccessPoints(); // This is a list of doors the user has access to.
//To get access point from its id
String id = "some_id"
AccessPoint accessPoint = permissionManager.getAccessPoint(id); //This will return null if no matching access point was found;
//To get access points of a specific organisation
String orgId = "some_id"
List<AccessPoint> accessPoints =
permissionManager.getAccessPointsOfOrganisation(orgId);
// To get update of when this list changes, attach an observer returned by the following function
Observable observable =
permissionManager.getAccessPointsUpdateObservable();
Observer observer = (observable, o) -> {
//Refetch the access points to keep yourself updated
List<AccessPoint> accessPoints = permissionManager.getAccessPoints(); });
observable.addObserver(observer);
//OR observe on AndroidX livedata
permissionManager.getAccessPointsLiveData().observe(this, accessPoints ->
{
//Use the accessPoints as needed
});
//This example is shown with lambda used in java 1.8. If you are using 1.7 then use anonymous class instead
//NOTE: Remove the observer once you no longer require to avoid memory leaks
class AccessPoint {
String id;
//Id of access point. Use this value to unlock access point List<String>;
//Ids of organisations the access point belongs to
String name;
//Name of the access point
List<String> deviceTags;
//Attached devices to access point (entry/exit)
boolean hasRemoteAccessPermission;
//Indicates whether user has permission to unlock this
//access point over internet
boolean hasProximityAccessPermission;
//Indicates whether user has permission to unlock this
//access point while walking towards it.
boolean hasTapToAccessPermission;
//Indicates whether user has permission to unlock this
//access point while walking towards it.
boolean hasClickToAccessPermission;
//Indicates whether user has permission to unlock this
//access point while when user requests it (app explicitly requests bleUnlock).
}
9 Starting the Bluetooth Scan
Once the user is logged in or once the user has opened the app and got the app in foreground, a check has to be made to use bluetooth based functionality i.e.
- For all Android versions, the bluetooth of the phone needs to be turned on for access to work via BLE.
- For Android version less than equal to 11: Check if bluetooth and location permissions are granted and prompt the user to turn on bluetooth and location. Here Nearby devices permission is not required.
- For Android version greater than equal to 12: Check if Nearby devices permission is granted and prompt the user to turn on bluetooth. Here the location permission and the tuning on of location is not required for access to work.
For android 6.0 and above till android 11.0, BLUETOOTH permission is required for bluetooth communication to work and the BLUETOOTH_ADMIN permission to scan for BLE devices and to be able to turn on the phone’s bluetooth.
Also, in these versions (android 6.0 and above till android 11.0), ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION permission is required and location (network mode) is to be turned on.
Along with that, for android 10.0 and above till android 11.0, the background location ACCESS_BACKGROUND_LOCATION permission is necessary for proximity/tap to access to work in the background.
For android 12.0 and above, the Nearby devices permission is required while the location permission or location turn on is not required. The Nearby devices permission in UI translates in code to BLUETOOTH_SCAN, BLUETOOTH_ADVERTISE and BLUETOOTH_CONNECT permissions, all of which are required. For the BLUETOOTH_SCAN permission a flag needs to assert that the scan is not for deriving physical location. See below how to mention it:
https://developer.android.com/develop/connectivity/bluetooth/bt-permissions#assert-never-for-location
This is a mandate required by android for scanning close by BLE devices. See more about requesting location permissions from the customer at runtime below:
https://developer.android.com/develop/sensors-and-location/location/permissions#request-location-access-runtime
Since BLUETOOTH, BLUETOOTH_ADMIN and the location related permissions are required only upto android 11.0, the maxSdkVersion needs to be set to 30 (android 11) for these permissions. See below how to use it:
https://developer.android.com/develop/connectivity/bluetooth/bt-permissions
Note: These permissions need to be requested from the user and granted along with turn on of bluetooth (and location in respective versions) before the next step of starting bluetooth scan of the access SDK. Since granting of these permissions requires user interactions it is left to the client app to implement it. However, for better understanding, below are examples of the dialogs
that should be presented to the customer to grant access in the respective scenarios. The client app can rely only on the system generated pop ups to get permissions from the user and to turn on the bluetooth and location. However, in-app pop ups with custom messages give the user more context on why the permissions and service turn on are required for access to work.
Bluetooth turn on in app prompt:
Bluetooth turn on system dialog:
Bluetooth Nearby Devices in app permission prompt:
Nearby Devices system permission dialog:
Location access permission in app prompt:
Location access permission system prompt:
Background location permission shown by app (for android 10 and 11):
Background location permission system settings (allow all the time should be selected below):
Location turn on in app prompt:
Location turn on system dialog:
The below snippet of code explains how to start the bluetooth scan.
Note: Without this step, the customer won't be able to access the doors to which they have access.
AccessManager accessManager = serviceProvider.getAccessManager();
accessManager.startBleScan();
// Make sure bluetooth is turned on and all the checks above are done before starting the scan.
//To observe on the scanned devices an AndroidX live data object is exposed by the sdk
LiveData<List<AccessPointBleDevice>> liveData =
accessManager.getScannedDevicesLiveData();
//The list is ordered such that the closest device is at the top.
//If devices are nearby
class AccessPointBleDevice {
String accessPointId;
//id of the access point the device is attached to
String tag;
//tag of the device(entry/exit) for the access point
boolean supportsCalibration;
//Whether the device allows mobile calibration on itself
int rssi;
//The last known signal strength of the device as measured by the phone.
//(Higher the value, closer the device)
}
Note: Along with starting of the scan each of these checks should be done each time the app comes in the foreground (not just when the app is opened) to account for the fact that the user might have turned off one of the required services or might have removed one of the permissions.
10 On Notifications
This snippet of the code explains the function that needs to be called whenever the access point permissions are updated in the backend (from the client backend to Spintly backend). When such permission changes happen, it’s the job of the client backend to alert the client app that permissions have changed. When the app receives the notification, it needs to call the poll data function. This will cause the SDK to receive the new permissions from the Spintly backend.
CloudSyncManager syncManager = serviceProvider.getCloudSyncManager();
syncManager.pollData(new CompletionCallback<Void>() {
@Override
public void completed(@Nullable Void result) {
//If the app needs new refreshed access points, it can call:
//permissionManager.getAccessPoints(); or permissionManager.getAccessPointsOfOrganisation(orgId); on this success
}
@Override
public void failed(@NonNull Throwable exc) {
//Handle the error
}
});
// Call this whenever you want to refresh the local sdk data with cloud
// The places to call this would be:
// 1. After login.
// 2. After changing the users permission (Notifications will be implemented in a later release).
//To set up regular polling you can call the following function
// PollingJobService.register(this, PollingJobService.DEFAULT_JOB_ID, PollingJobService.DEFAULT_INTERVAL);
//This registers a scheduled job service with given interval and job id // DEFAULT_INTERVAL = 8hrs
// DEFAULT_JOB_ID = 1
11 Opening the Doors
Spintly provides 3 options to open the doors:
Click to Access: This works well for use cases of listing all the doors in the app UI such that the customer can see them, and click on a specific door to unlock it
Tap to Access: Here user just needs to tap the phone on the unit and the door will open
Proximity Access: Here the phone needs to be within the proximity of the device
The tap to access and proximity access can be enabled or disabled in the Spintly backend.
Click to Access
If he wants to list the doors and wants to open the door by click to access, then he needs to use the following snippet.
UnlockCallback callback = new UnlockCallback() {
/**
* Indicates when a device is selected for unlocking.
* If the scan is running but the device was not detected till that instant, the service waits for a fixed time before giving an error.
*
* @param accessPointId the id of the access point to which the device is attached
* @param tag the tag of device within the access point
*/
default void onDeviceFound(@NonNull String accessPointId, @NonNull String tag) {}
/**
* Indicates that the device is unlocked successfully
* @param accessPointId the id of the access point to which the device is attached
* @param tag the tag of device within the access point
* @param customParameter Custom parameter that was passed while requesting unlock. If nothing was passed this will be 0x00
*/
void onSuccess(@NonNull String accessPointId, @NonNull String tag, byte customParameter);
/**
* Indicates any error encountered during unlock or timeout
*
* @param exception The cause of failure
*/
void onFailure(@NonNull Exception exception);
}
AccessPoint requestedPoint = accessPoints.get(n);
// Requested access to a specific access point
AccessManager accessManager = serviceProvider.getAccessManager(); accessManager.bleUnlockAccessPoint(requestedPoint.id, callback);
//If you would like to add a custom parameter you can add it the following way
byte customParameter = (byte) 1; //(between 0 to 255)
// 1 byte unsigned int custom parameter can be added to send in access history.
accessManager.bleUnlockAccessPoint(requestedPoint.id, customParameter, callback);
// Only one unlock at a time is supported as of this moment so please avoid calling this function again without the previous one returning success/error.
Proximity Access
The developer need not call any extra SDK function in the app for tap to access and proximity to work. However, if callbacks on successful and failed unlocks are required, the below should be followed.
//This step is optional. Tap to access unlock will occur when the phone is tapped on the access points and proximity unlock will occur on access points when the phone comes in its proximity (which have it enabled in the backend),
// regardless of setting this callback
//If you need to observe every proximity unlock then add this step in application onCreate()
//Using this the app can identify on which access point the tap to access was successful
//and showing the customer the access point name on which it unlocked by using the name of the access point
//from the list of access points by searching for the accessPointId returned in the onSuccess()
AccessManager accessManager = serviceProvider.getAccessManager();
accessManager.setProximityCallback(new ProximityCallback() {
@Override
public void onSuccess(@NonNull String accessPointId, @NonNull String tag) {
//App can use the returned info for display purposes
//or show a general unlock success message
}
@Override
public void onFailure(@NonNull Exception exception) {
//Handle error of unlock
}
});
12 Remote Unlock
There can be situations wherein the BLE unlock can fail if the access point device is out of BLE range. In such a scenario the client app can choose to use the remote unlock functionality of the access SDK. This basically means that the user now has the functionality of unlocking the access point via the internet. The access point needs to be connected to a gateway and needs to have online status for this method of unlocking to work.
Alternatively, the client app can present to the user the option to unlock an access point remotely without trying BLE unlock at all if it is confirmed that a user is in a location other than that of BLE range of the access point of interest.
The client app can choose to give access to this functionality to select users or to all. The client backend can as a good measure disable the remote unlock feature on an access point for a user via the Spintly backend.
In the image below, the scenario of BLE unlock failure circumstance in which the client app chooses to initiate remote unlock is covered.
- App calls the BLE unlock access point (click to access) function of SDK.
- BLE unlock success or failure from SDK to app.
- If BLE unlock fails, the app can choose to call the remote unlock access point function of SDK.
- However the app needs to check the hasRemoteAccessPermission boolean of that particular access point in the access points before proceeding further.
- Remote unlock success or failure from SDK to app.
Below is the snippet of code to initiate remote unlock:
RemoteUnlockCallback callback = new RemoteUnlockCallback() {
/**
* Indicates that the access point is unlocked successfully
* @param accessPointId the id of the access point
*/
void onSuccess(@NonNull String accessPointId);
/**
* Indicates any error encountered during unlock or timeout
*
* @param exception The cause of failure
*/
void onFailure(@NonNull Exception exception);
};
accessManager.remoteUnlockAccessPoint(requestedPoint.id, callback);
13 Mobile NFC access
The Spintly access SDK supports NFC based access for mobile phones which have NFC hardware. For NFC access to work, it is the responsibility of the client app to show the user prompts to turn on the mobile’s NFC if the mobile has NFC hardware. If these requirements are fulfilled then NFC based tap to access will work if access SDK login is done. However, NFC based access only supports tap to access and will not support other modes via pure NFC.
Tap to access can then be used only via NFC and turning off bluetooth. However, it is advised to suggest the users to use both NFC and bluetooth for seamless access.
For better understanding, below are the dialogs that should be presented to the customer to grant access to phone NFC.
NFC turn on in app prompt:
NFC system settings:
Note: Each time the app comes into the foreground, it is the responsibility of the client app to show a prompt to the user if the NFC is not turned on which has to be independent of and parallel to BLE pop ups.
14 Door State Change
Apart from the unlock functionality, the client app has the option to also change the state of the access point device between locked, unlocked and access control using the access SDK. It is, however, advised to make this option available in the client app UI to only select users like admins and other related privileged users. Below is the code snippet to set the door state:
SetDoorStateCallback callback = new SetDoorStateCallback {
/**
* Indicates that the device is unlocked successfully
* @param accessPointId the id of the access point to which the device is attached
* @param newDoorState New door state on the device
* @param oldDoorState New door state on the device
*/
void onSuccess(@NonNull String accessPointId, byte newDoorState, byte oldDoorState);
/**
* Indicates any error encountered during unlock or timeout
*
* @param exception The cause of failure
*/
void onFailure(@NonNull Exception exception);
}
//byte doorState, newDoorState and oldDoorState can have the values:
//Access control: 0x01
//Unlocked: 0x02
//Locked: 0x03
byte doorState = 0x02; //Unlocked state
accessManager.bleSetDoorState(requestedPoint.id, doorState, callback);
15 On logout
When the user logs out of the app, it is also important that the app first stops the BLE scan and then logs out from both the SDK i.e. the OAuth SDK and Access Control SDK. The following snippet of code does the job.
accessManager.stopBleScan();
credentialManager.logOut()
spintlyOauthManager.clearSession();