Skip to content

Getting Started - Android

Welcome to the introductory guide for developing Android applications that interact with flinkey boxes using the Tapkey Mobile SDK. This document provides step-by-step instructions to get you started with integrating flinkey functionalities in your Android apps.

For practical implementation, we provide a Sample Application on GitHub that demonstrates how to use the Tapkey Mobile SDK within an Android app. You can access and clone the repository here:

Additionally, extensive documentation on the Tapkey Mobile SDK is available at:

We recommend thoroughly exploring both the sample application and the Tapkey documentation to gain a comprehensive understanding of the capabilities and integration techniques.

Prerequisites

  • flinkey box with firmware version 30 or newer
  • flinkey API access

1. Library dependencies

There are three libraries to be added as dependencies to your Android project: The Tapkey Mobile SDK the WITTE Mobile Library and the AppAuth for Android.

Library dependencies diagram

The Tapkey Mobile SDK enables mobile apps to interact with flinkey boxes on both Android and iOS platforms. It manages digital keys by retrieving them from the Tapkey Trust Service and storing them locally for offline use.

The WITTE Mobile Library supplements the Tapkey Mobile SDK with additional functionalities specific to flinkey box operations. It includes features for converting flinkey box IDs to physical lock IDs and vice versa, and interpreting the feedback from the flinkey box to determine its open or closed state. The library aids in the integration of Tapkey Mobile SDK within mobile applications, enhancing the management and operation of flinkey boxes.

AppAuth for Android is a client SDK that facilitates communication with OAuth 2.0 and OpenID Connect providers. It is designed to support Android applications by providing a secure and efficient way to implement user authentication and authorization.

1.1. Tapkey Mobile SDK

The Tapkey Mobile SDK is distributed through Tapkey's own maven repository server. Before you can reference the Tapkey Mobile SDK the corresponding maven repository needs to be added to the list of repositories in the build.gradle file.

repositories {
    //...
    // Maven repository for the Tapkey Mobile SDK.
    maven { url "https://maven.tapkey.com" }
}

Add the Tapkey Mobile Library reference to your build.gradle file.

dependencies {
    //...
    // Tapkey Mobile SDK
    api 'com.tapkey.android:Tapkey.MobileLib:<latest_version>'
}

1.2 WITTE Mobile Library

The WITTE Mobile library is distributed via GitHub packages. Add this additional maven repository to your build.gradle file.

repositories {
    //...
    maven {
            url "https://maven.pkg.github.com/WITTE-Digital/witte-mobile-library-for-android"
            credentials {
                username = <GitHub user name>
                password = <GitHub personal access token>
            }
        }
}

Add the witte-mobile-library reference to your gradle.build file.

dependencies {
    //...
    implementation 'digital.witte:witte-mobile-library:3.0.0'
}

1.3 AppAuth for Android

The AppAuth library is available via Maven Central which is typically one of the default repositories in most Android projects.

repositories {
    //...
    mavenCentral()
}

Add the appauth reference to your gradle.build file.

dependencies {
    //...
    implementation 'net.openid:appauth:<version_compatible_with_your_setup>'
}

2. Authentication

The Tapkey Mobile SDK needs to communicate with the Tapkey Trust Service to synchronize digital keys with the mobile device for authenticated users. It is the responsibility of the mobile app to provide an access token to the Tapkey Mobile SDK upon login and subsequently on token refresh requests by the Tapkey Mobile SDK. The access token is retrieved by obtaining an IdToken first which is then exchanged for an access token.

Library dependencies diagram

2.1 IdToken

It is assumed that the mobile app has its own backend including user management for authentication and authorization. Since the flinkey API is designed for server to server communication only the mobile app cannot query the flinkey API directly. To obtain an IdToken the mobile app needs to query its own backend in a secure way. The app backend needs to handle this request by interacting with the flinkey API to get an IdToken for a specific user. Please refer to the section App SDK of the flinkey API v3 documentation.

2.2 Token Exchange

The IdToken can be exchanged for an access token utilizing functions provided by the AppAuth library. The following source code snippets show the basic usage. Please note that there is complete implementation of the token exchange available as part of the sample application.

private final static String AuthConfigScheme = "https";
private final static String AuthConfigAuthority = "login.tapkey.com";
private final static String TokenExchangeClientId = "wma-native-mobile-app";
private final static String TokenExchangeGrantType = "http://tapkey.net/oauth/token_exchange";
private final static String TokenExchangeScopeRegisterMobiles = "register:mobiles";
private final static String TokenExchangeScopeReadUser = "read:user";
private final static String TokenExchangeScopeHandleKeys = "handle:keys";
private final static String TokenExchangeParamProviderKey = "provider";
private final static String TokenExchangeParamSubjectTokenTypeKey = "subject_token_type";
private final static String TokenExchangeParamSubjectTokenKey = "subject_token";
private final static String TokenExchangeParamAudienceKey = "audience";
private final static String TokenExchangeParamRequestedTokenTypeKey = "requested_token_type";
private final static String TokenExchangeParamProviderValue = "wma.oauth";
private final static String TokenExchangeParamSubjectTokenTypeValue = "jwt";
private final static String TokenExchangeParamAudienceValue = "tapkey_api";
private final static String TokenExchangeParamRequestedTokenTypeValue = "access_token";

The method AuthorizationServiceConfiguration.fetchFromIssuer asynchronously fetches the configuration from the authorization server’s well-known configuration endpoint. The result of this operation together with the IdToken and constants provided above are used to build a TokenRequest. The TokenRequest object is then used to query the access token.

import net.openid.appauth.AuthorizationService;
import net.openid.appauth.AuthorizationServiceConfiguration;
import net.openid.appauth.TokenRequest;

String idToken = "<IdToken>";

Uri.Builder builder = new Uri.Builder();
Uri authorizationServer = builder
    .scheme(AuthConfigScheme)
    .encodedAuthority(AuthConfigAuthority)
    .build();

AuthorizationServiceConfiguration.fetchFromIssuer(authorizationServer, (serviceConfiguration, fetchFromIssuerException) -> {
    TokenRequest.Builder tokenRequestBuilder =
        new TokenRequest.Builder(serviceConfiguration, TokenExchangeClientId)
            .setCodeVerifier(null)
            .setGrantType(TokenExchangeGrantType)
            .setScopes(new HashSet<String>() {{
                add(TokenExchangeScopeRegisterMobiles);
                add(TokenExchangeScopeReadUser);
                add(TokenExchangeScopeHandleKeys);
            }})
            .setAdditionalParameters(new HashMap<String, String>() {{
                put(TokenExchangeParamProviderKey, TokenExchangeParamProviderValue);
                put(TokenExchangeParamSubjectTokenTypeKey, TokenExchangeParamSubjectTokenTypeValue);
                put(TokenExchangeParamSubjectTokenKey, idToken);
                put(TokenExchangeParamAudienceKey, TokenExchangeParamAudienceValue);
                put(TokenExchangeParamRequestedTokenTypeKey, TokenExchangeParamRequestedTokenTypeValue);
            }});

    TokenRequest tokenRequest = tokenRequestBuilder.build();

    AuthorizationService authService = new AuthorizationService(_context);
    authService.performTokenRequest(tokenRequest, (response, performTokenRequestException) -> {
        String accessToken = response.accessToken;
    });
});
Please remember to replace the placeholder with the actual IdToken you have received from your service before usage.

3. Working with the Tapkey Mobile SDK

3.1 TapkeyServiceFactory

The TapkeyServiceFactory serves as the pivotal component of the Tapkey Mobile SDK. Primarily, it simplifies the creation and accessibility of various objects instrumental in executing an array of responsibilities, which include user authentication, digital key management, and facilitating communication with the flinkey box. The example below demonstrates how to create an instance of TapkeyServiceFactory within the onCreate() method in your Application class. Instantiate TapkeyEnvironmentConfig and configure it using TapkeyEnvironmentConfigBuilder. The setTenantId() method sets the tenant ID. Instantiate TapkeyBleAdvertisingFormat and further configure it using TapkeyBleAdvertisingFormatBuilder. At this stage, you're including formats v1 and v2. Lastly, create the TapkeyServiceFactory instance.

Here's how you can achieve these steps:

import com.tapkey.mobile.*;
import com.tapkey.mobile.auth.TokenRefreshHandler;
import com.tapkey.mobile.ble.TapkeyBleAdvertisingFormat;
import com.tapkey.mobile.ble.TapkeyBleAdvertisingFormatBuilder;
import com.tapkey.mobile.concurrent.CancellationToken;
import com.tapkey.mobile.concurrent.Promise;
import digital.witte.wittemobilelibrary.Configuration;

public class App extends Application {
    private TapkeyServiceFactory mTapkeyServiceFactory; // This is where we store a reference to the TapkeyServiceFactory

    @Override
    public void onCreate() {
        super.onCreate();

        TapkeyEnvironmentConfig tapkeyEnvironmentConfig = new TapkeyEnvironmentConfigBuilder()
                .setTenantId(Configuration.TenantId)
                .build();

        TapkeyBleAdvertisingFormat bleAdvertisingFormat = new TapkeyBleAdvertisingFormatBuilder()
                .addV1Format(Configuration.BleAdvertisingFormatV1)
                .addV2Format(Configuration.BleAdvertisingFormatV2)
                .build();

        mTapkeyServiceFactory = new TapkeyServiceFactoryBuilder(this)
                .setConfig(tapkeyEnvironmentConfig)
                .setBluetoothAdvertisingFormat(bleAdvertisingFormat)
                .setTokenRefreshHandler(new TokenRefreshHandler() {
                    @Override
                    public Promise<String> refreshAuthenticationAsync(String s, CancellationToken cancellationToken) {
                        // Implement the logic to retrieve the access token here
                    }
                    @Override
                    public void onRefreshFailed(String tapkeyUserId) {
                        // Implement error handling logic here
                    }
                })
                .build();

        // Further logic...
    }
}

The method .setTokenRefreshHandler() is used to set a token refresh handler to the TapkeyServiceFactory instance. The token refresh handler is responsible for refreshing the user's authorization token when it expires, thereby ensuring uninterrupted communication with the Tapkey services. This method of the TapkeyServiceFactoryBuilder accepts an instance of TokenRefreshHandler as an argument. TokenRefreshHandler is an interface where you need to implement two methods:

  1. refreshAuthenticationAsync(): This method is invoked when Tapkey Service Factory requires a new access token. It's given the user's tapkeyUserId and a CancellationToken. This method should return a promise object that completes with the new access token or fails if the access token could not be obtained. It should handle any asynchronous operation required to get the new access token (e.g., a request to your backend), but also handle the cancellation request signalized by the CancellationToken.
  2. onRefreshFailed(): This method is invoked whenever refreshing of the access token fails. Tapkey invokes this method with the tapkeyUserId as argument. If refreshing the access token fails, this method is called, and you can define how the system behaves (retry, notify the user, etc.) within this method. It allows you to handle any necessary clean-up or error logging procedures.

3.2 Login

After obtaining an access token, you're all set to authenticate with the Tapkey Mobile SDK. For authentication, we will use the UserManager instance, which is accessible through the TapkeyServiceFactory.

The following Java example demonstrates how you can authenticate a user with the Tapkey Mobile SDK:

// authenticate a user with the Tapkey Mobile SDK
UserManager userManager = mTapkeyServiceFactory.getUserManager();
userManager.logInAsync(accessToken, CancellationTokens.None)
    .continueOnUi(userId -> {
        // login successful
    })
    .catchOnUi(e -> {
        // login failed
        return null;
    });
In this example, the logInAsync method is used for the login operation. This method uses the access token (obtained earlier) and an instance of CancellationTokens.None (indicating cancellation isn't desired) as its arguments. Success or failure of the operation can be handled within continueOnUi and catchOnUi blocks respectively.

3.3 Scanning

A flinkey box is a Bluetooth Low Energy (BLE) device. Although it's not mandatory to pair a flinkey box with your mobile device, the Tapkey Mobile SDK needs to locate nearby flinkey Boxes via a scan before it can establish any connection required to control the flinkey box.

Below is a Java code snippet demonstrating how to utilize the SDK:

// Acquire an instance of BleLockScanner from the TapkeyServiceFactory.
BleLockScanner bleLockScanner = mTapkeyServiceFactory.getBleLockScanner();

// Initiate the scanning process for flinkey Boxes.
if(mForegroundScanRegistration == null) {
    mForegroundScanRegistration = mBleLockScanner.startForegroundScan();
}

// The scanning for flinkey Boxes can be stopped as follows.
if(mForegroundScanRegistration != null){
    mForegroundScanRegistration.close();
    mForegroundScanRegistration = null;
}

The startForegroundScan() method from the BleLockScanner instance commences the scanning for flinkey boxes. The close() method stops the ongoing scan.

The mForegroundScanRegistration is used to manage the scanning operation, closing it when it's not needed anymore, and ensuring there's no ongoing scan when starting a new one. It's strongly recommended to manage the scanning process properly to prevent unnecessary use of resources.

3.4 Triggering

The process of 'Triggering' a flinkey box refers to changing its status from open to closed, or the other way around. This can be achieved by using the executeStandardCommandAsync method provided in the Mobile SDK. When this method is invoked, it returns a CommandResult object, which can be examined to identify the current status of the flinkey box.

Converting the flinkey box ID into a physical lock ID that is compatible with the Tapkey Mobile SDK is an essential step. This is accomplished using the BoxIdConverter, a part of the WITTE Mobile Library.

Below is the code snippet demonstrating the usage of executeStandardCommandAsync method to toggle a flinkey box's status:

import digital.witte.wittemobilelibrary.box.BoxFeedback;
import digital.witte.wittemobilelibrary.box.BoxFeedbackV3;
import digital.witte.wittemobilelibrary.box.BoxFeedbackV3Parser;
import digital.witte.wittemobilelibrary.box.BoxIdConverter;

CommandExecutionFacade commandExecutionFacade = mTapkeyServiceFactory.getCommandExecutionFacade();
BleLockCommunicator bleBleLockCommunicator = mTapkeyServiceFactory.getBleLockCommunicator();
BleLockScanner bleLockScanner = mTapkeyServiceFactory.getBleLockScanner();

// The flinkey box ID is printed on the box's label
String boxId = "C3-B2-C3-D4";

// Convert the box ID to a physical lock ID for use with Tapkey Mobile SDK 
String physicalLockId = BoxIdConverter.toPhysicalLockId(boxId);

// Get the bluetooth address of the flinkey box
String bluetoothAddress = bleLockScanner.getLock(physicalLockId).getBluetoothAddress();

// Timeout and cancellation
final int timeoutInMs = 60 * 1000;
CancellationToken timeout = CancellationTokens.fromTimeout(timeoutInMs);

mBleBleLockCommunicator.executeCommandAsync(
    bluetoothAddress,
    physicalLockId,
    tlcpConnection ->
    {
        TriggerLockCommand triggerLockCommand = new DefaultTriggerLockCommandBuilder().build();
        return mCommandExecutionFacade.executeStandardCommandAsync(tlcpConnection, triggerLockCommand, timeout);
    },
    timeout).continueOnUi(commandResult -> {
        CommandResult.CommandResultCode commandResultCode = commandResult.getCommandResultCode();
        switch (commandResultCode) {
            case Ok: {
                // Fetch the box feedback from the command result
                Object object = commandResult.getResponseData();
                if (object instanceof byte[]) {
                    byte[] responseData = (byte[]) object;
                    if (10 == responseData.length) {
                        BoxFeedback boxFeedback = BoxFeedback.create(responseData);
                        // Evaluate box feedback
                    }
                    else {
                        BoxFeedbackV3 boxFeedbackV3 = BoxFeedbackV3Parser.parse(responseData);
                        // Evaluate box feedback
                    }                    
                }
            }
        }
    )}.finallyOnUi(() -> {
        // Termination handling code
    )}.catchOnUi(e -> {
        // Exception handling code
    });

The above code will execute the TriggerLockCommand and then evaluate the CommandResult to fetch the box's feedback. Based on the length of the response data, it either creates a BoxFeedback or BoxFeedbackV3 object. The feedback is then evaluated to understand the status of the box.