Skip to content

2. Usage

Note

We're sorry, this article hasn't been completed or updated yet. We're working on finishing it as soon as possible. In case of any questions, please reach out to our Support Team.

Basic Usage

This section will show you basics of the Kontakt SDK and explain concepts of ProximityManager and other components.

Scanning devices is being done by a ProximityManager, a high level component that allows scanning both for shuffled and normal devices.

More detailed information can be found in JavaDocs.

Ranging & Monitoring Devices

Scanning beacons can be done in two ways:

  • Ranging
  • Monitoring

Ranging begins scan for devices instantly and keeps scanning constantly. Monitoring, on the other hand, scans periodically.

Below you can find most basic example that performs scanning for IBeacons and Eddystones and prints a log when a device is discovered, considering that BLE is already enabled and Android Marshmallow permissions are granted (if you are using device with Android Marshmallow).

Scanning is performed with default configuration, regions and filters. You can read more about configurating ProximityManager later on.

public class MainActivity extends AppCompatActivity {

  private ProximityManager proximityManager;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    KontaktSDK.initialize("YOUR_API_KEY");

    proximityManager = ProximityManagerFactory.create(this);
    proximityManager.setIBeaconListener(createIBeaconListener());
    proximityManager.setEddystoneListener(createEddystoneListener());
  }

  @Override
  protected void onStart() {
    super.onStart();
    startScanning();
  }

  @Override
  protected void onStop() {
    proximityManager.stopScanning();
    super.onStop();
  }

  @Override
  protected void onDestroy() {
    proximityManager.disconnect();
    proximityManager = null;
    super.onDestroy();
  }

  private void startScanning() {
    proximityManager.connect(new OnServiceReadyListener() {
      @Override
      public void onServiceReady() {
        proximityManager.startScanning();
      }
    });
  }

  private IBeaconListener createIBeaconListener() {
    return new SimpleIBeaconListener() {
      @Override
      public void onIBeaconDiscovered(IBeaconDevice ibeacon, IBeaconRegion region) {
        Log.i("Sample", "IBeacon discovered: " + ibeacon.toString());
      }
    };
  }

  private EddystoneListener createEddystoneListener() {
    return new SimpleEddystoneListener() {
      @Override
      public void onEddystoneDiscovered(IEddystoneDevice eddystone, IEddystoneNamespace namespace) {
        Log.i("Sample", "Eddystone discovered: " + eddystone.toString());
      }
    };
  }
}

IBeacon & Eddystone Listeners

Each listener provides 3 callbacks. See IBeaconListener example below (EddystoneListener is analogous)

proximityManager.setIBeaconListener(new IBeaconListener() {
  @Override
  public void onIBeaconDiscovered(IBeaconDevice iBeacon, IBeaconRegion region) {
    //Beacon discovered
  }

  @Override
  public void onIBeaconsUpdated(List<IBeaconDevice> iBeacons, IBeaconRegion region) {
    //Beacons updated
  }

  @Override
  public void onIBeaconLost(IBeaconDevice iBeacon, IBeaconRegion region) {
    //Beacon lost
  }
});
  • onIBeaconDiscovered - device has been discovered (1 device, 1 space)

  • onIBeaconsUpdated - discovered devices have been updated (n devices, 1 space)

  • onIBeaconLost - eariler discovered device has been lost (is considered inactive)

Both IBeacon and Eddystone listeners also come with simplified versions if you are interested only in a particular event. For example:

//I am only interested in discovering iBeacons

proximityManager.setIBeaconListener(new SimpleIBeaconListener() {
  @Override
  public void onIBeaconDiscovered(IBeaconDevice ibeacon, IBeaconRegion region) {
    //Beacon discovered
  }
});

Secure Profile Listener (Beacon Pro)

Secure Profile is a special configuration frame that is broadcasted only by Beacon Pro devices. This frame contains data required to connect and configure Beacon Pro devices. This listener can be used the same way as IBeacon and Eddystone:

proximityManager.setSecureProfileListener(new SecureProfileListener() {
    @Override
    public void onProfileDiscovered(ISecureProfile profile) {
        //Profile discovered
    }

    @Override
    public void onProfilesUpdated(List<ISecureProfile> profiles) {
        //Profiles updated
    }

    @Override
    public void onProfileLost(ISecureProfile profile) {
        //Profile lost
    }
});

It also comes with simplified version:

proximityManager.setSecureProfileListener(new SimpleSecureProfileListener() {
    @Override
    public void onProfileDiscovered(ISecureProfile profile) {
        //Profile discovered
    }
});

Kontakt Telemetry (Beacon Pro)

Starting with Beacon Pro firmware version 1.10 it is possible to keep track of telemetry data. Currently broadcasted data are:

  • Accelerometer sensor readings
  • Temperature (Celsius degrees)
  • Light level (0 - 100%)
  • UTC device time

Telemetry could be extracted from ISecureProfile objects:

proximityManager.setSecureProfileListener(new SimpleSecureProfileListener() {
    @Override
    public void onProfileDiscovered(ISecureProfile profile) {
        // Extract telemetry
        KontaktTelemetry telemetry = profile.getTelemetry();
        if (telemetry != null) {
            // Get acceleration, temperature, light level and device time
            Acceleration acceleration = telemetry.getAcceleration();
            int temperature = telemetry.getTemperature();
            int lightLevel = telemetry.getLightSensor();
            int deviceTime = telemetry.getTimestamp();
        }
    }
});

Space Listener

If you are interested specifically in monitoring IBeacon regions and Eddystone namespaces entering / abandoning you can add a SpaceListener callback that will be invoked when such events occur. See example below.

proximityManager.setSpaceListener(new SpaceListener() {
    @Override
    public void onRegionEntered(IBeaconRegion region) {
        //IBeacon region has been entered
    }

    @Override
    public void onRegionAbandoned(IBeaconRegion region) {
        //IBeacon region has been abandoned
    }

    @Override
    public void onNamespaceEntered(IEddystoneNamespace namespace) {
        //Eddystone namespace has been entered
    }

    @Override
    public void onNamespaceAbandoned(IEddystoneNamespace namespace) {
        //Eddystone namespace has been abandoned
    }
});
  • onRegionEntered - invoked when IBeacon region has been entered

  • onRegionAbandoned - invoked when IBeacon region has been abandoned

  • onNamespaceEntered - invoked when Eddystone namespace has been entered

  • onNamespaceAbandoned - invoked when Eddystone namespace has been abandoned

You can also use simple version if you are only interested in particular events. For example:

proximityManager.setSpaceListener(new SimpleSpaceListener() {
  @Override
  public void onRegionEntered(IBeaconRegion region) {
    //IBeacon region has been entered
  }

  @Override
  public void onNamespaceEntered(IEddystoneNamespace namespace) {
    //Eddystone namespace has been entered
  }
});

There is also IBeaconSpaceListener and EddystoneSpaceListener available for your convenience.

Scan Status Listener

ScanStatusListener informs you when scanning is started, stopped or when an error occurs. See example below.

proximityManager.setScanStatusListener(new ScanStatusListener() {
      @Override
      public void onScanStart() {
        //Scan started
      }

      @Override
      public void onScanStop() {
        //Scan stopped
      }

      @Override
      public void onScanError(ScanError error) {
        //Error occured
      }

      @Override
      public void onMonitoringCycleStart() {
        //Monitoring cycle started
      }

      @Override
      public void onMonitoringCycleStop() {
        //Monitoring cycle finished
      }
});
  • onScanStart - invoked when scanning is started

  • onScanStop - invoked when scanning is stopped

  • onScanError - invoked if an error occurs during a scan or scan initialization.

  • onMonitoringCycleStart - invoked when monitoring cycle starts (invoked only if ScanPeriod is set to other than RANGING)

  • onMonitoringCycleStop - invoked when monitoring cycle starts (invoked only if ScanPeriod is set to other than RANGING)

You can also use simple version if you are only interested in particular events. For example:

proximityManager.setScanStatusListener(new SimpleScanStatusListener() {
      @Override
      public void onScanStart() {
        //Scan started
      }

      @Override
      public void onScanStop() {
        //Scan stopped
      }
});

Configuration

To start ProximityManager configuration call configure() method on a ProximityManager object. Configuration should be done before starting a scan with startScanning() method.

All possible settings are listed below:

proximityManager.configuration()
    .scanMode(ScanMode.BALANCED)
    .scanPeriod(ScanPeriod.RANGING)
    .activityCheckConfiguration(ActivityCheckConfiguration.DISABLED)
    .forceScanConfiguration(ForceScanConfiguration.DISABLED)
    .deviceUpdateCallbackInterval(TimeUnit.SECONDS.toMillis(5))
    .rssiCalculator(RssiCalculators.DEFAULT)
    .cacheFileName("Example")
    .resolveShuffledInterval(3)
    .monitoringEnabled(true)
    .monitoringSyncInterval(10)
    .eddystoneFrameTypes(Arrays.asList(EddystoneFrameType.UID, EddystoneFrameType.URL))
  • Scan Mode - determines scan performance. Android Lollipop and Marshmallow support three scan modes: Balanced, Low Latency or Low Power.

  • Scan Period - determines scan duration and intervals. Can be set to one of the presets: Ranging, Monitoring or user's own configuration by passing new ScanPeriod instance.

  • Activity Check Configuration - defines frequency of inactive beacon check, and time before an absent beacon is considered inactive and is reported lost.

  • ForceScan Configuration - forces a period scan restart. See Known Issues section for more info. This option will always be disabled for Android N or higher (Android N introduced a restriction where you can only start/stop BLE scan 5 times in 30 seconds window).

  • Device Update Callback Interval - prevents update callbacks from being called instantly. See Best Practices section for more info.

  • Rssi Calculator - sets custom calculator for RSSI filtering and other manipulations.

  • Cache File Name - sets file name stored in internal memory of application (data/data directory) with already resolved shuffled beacons.

  • Resolve Shuffled Interval - sets delay between trying to resolve shuffled beacons in seconds.

  • Monitoring Enabled - with Android SDK version 2.1.2 we introduced monitoring beacon battery level and send that info to Kontakt Cloud. This is enabled by default.

  • Monitoring Sync Interval - sets interval between sending monitoring events to the cloud.

  • Eddystone Frame Types - required Eddystone frames. See Eddystone Frames Selection section for more info.

Space Selection

With the introduction of Eddystone support, the concept of a Space was introduced - this provides a common abstraction for iBeacon Regions and Eddystone Namespaces.

IBeacon Regions

By default ProximityManager is built with BeaconRegion.EVERYWHERE region which accepts all Kontakt.io iBeacon profile beacons. Such configuration results that all beacons are put in the same Space.

Supporting more regions in your application can be done by setting IBeaconRegions to ProximityManager.

Collection<IBeaconRegion> beaconRegions = new ArrayList<>();

IBeaconRegion region1 = new BeaconRegion.Builder()
    .identifier("My Region")
    .proximity(UUID.fromString("6565d504-e306-4119-8266-0f8d4401cd0a"))
    .major(123)
    .minor(45)
    .build();

IBeaconRegion region2 = new BeaconRegion.Builder()
    .identifier("My second Region")
    .proximity(UUID.fromString("6565d504-e306-4119-8266-0f8d4401cd0a"))
    .major(BeaconRegion.ANY_MAJOR) //any major, default value
    .minor(BeaconRegion.ANY_MINOR) //any minor, default value
    .build();

beaconRegions.add(region1);
beaconRegions.add(region2);

proximityManager.spaces().iBeaconRegions(beaconRegions);

Eddystone Namespaces

In the same way, ProximityManager by default is built with EddystoneNamespace.EVERYWHERE, which accepts all Kontakt.io Eddystone profile beacons.

Supporting more namespaces in your application can be done by setting IEddystoneNamespaces to ProximityManager.

Collection<IEddystoneNamespace> eddystoneNamespaces = new ArrayList<>();

IEddystoneNamespace namespace1 = new EddystoneNamespace.Builder()
    .identifier("My Namespace")
    .namespace("f7826da64fa24e988024")
    .instanceId("744653683700")
    .build();

eddystoneNamespaces.add(namespace1);

proximityManager.spaces().eddystoneNamespaces(eddystoneNamespaces);

Filtering

IBeacon Filters

You can add filters to refine your iBeacon device selection. The IBeaconFilters class accepts the following filters:

  • ProximityUUIDFilter
  • MajorFilter
  • MinorFilter
  • IBeaconUniqueIdFilter
  • IBeaconMultiFilter
  • FirmwareFilter
  • DeviceNameFilter

Below snippet adds a filter for Proximity UUID, Major and Minor values.

List<IBeaconFilter> filterList = Arrays.asList(
       IBeaconFilters.newProximityUUIDFilter(UUID.fromString("f7826da6-4fa2-4e98-8024-bc5b71e0893e")),
       IBeaconFilters.newMajorFilter(43),
       IBeaconFilters.newMinorFilter(34)
);

proximityManager.filters().iBeaconFilters(filterList);

Custom IBeacon Filters

You can create your own custom filters based on IBeaconDevice:

IBeaconFilter customIBeaconFilter = new IBeaconFilter() {
    @Override
    public boolean apply(IBeaconDevice iBeaconDevice) {
        //Do your logic here. For example:
        return "Q2eZ".equals(iBeaconDevice.getUniqueId());
    }
};

proximityManager.filters().iBeaconFilter(customIBeaconFilter);

Eddystone Filters

You can add filters to refine your Eddystone device selection. The following filters can be applied:

  • NamespaceFilter
  • InstanceIdFilter
  • URLFilter
  • UIDFilter
List<EddystoneFilter> filtersList = Arrays.asList(
    EddystoneFilters.newUIDFilter("f7826da6bc5b71e0893e", "865283"),
    EddystoneFilters.newURLFilter("http://kontakt.io")
);

proximityManager.filters().eddystoneFilters(filtersList);

Custom Eddystone Filters

You can create your own custom filters based on IEddystoneDevice:

EddystoneFilter eddystoneFilter = new EddystoneFilter() {
  @Override
  public boolean apply(IEddystoneDevice device) {
    //Do your logic here. For example:
    return device.getUrl().contains("beacons") && "aBcD".equals(device.getUniqueId());
  }
};

proximityManager.filters().eddystoneFilter(eddystoneFilter);

Logging

public class App extends Application {

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

       KontaktSDK.initialize(this)
               .setDebugLoggingEnabled(BuildConfig.DEBUG)
               .setLogLevelEnabled(LogLevel.DEBUG, true);
   }
}

The Kontak.io SDK class is also the place to set logging as illustrated in the initialize example above.

To view the log output:

Open the Android DDMS tool window (click Android in the status bar). To filter messages from the SDK, enter kontakt.io into the search box and press enter.

Eddystone Frames Selection

The eddystone format currently consists of three frames:

  • UID
  • URL
  • TLM
  • EID (Beacon Pro)
  • ETLM (Beacon Pro)

To meet this specification we introduced the possibility to choose which frames are required when a particular beacon is scanned.

By default you are not required to specify any particular frame type. Eddystone will be reported as soon as possible but you won't know which frame data is available in onEddystoneDiscovered callback.

Specifying the required frame types guarantees that the parameters dependent on these frame types will never be null. For example:

proximityManager.configuration().eddystoneFrameTypes(EnumSet.of(
    EddystoneFrameType.UID, 
    EddystoneFrameType.URL)
);

The above example tells the SDK that the event for a particular Eddystone can be raised only when UID and URL frames were advertised and parsed.

Another example. If you are only interested URL broadcast you can set only one required frame:

proximityManager.configuration().eddystoneFrameTypes(EnumSet.of(EddystoneFrameType.URL));

This will guarantee that reported IEddystoneDevice object's URL property will never be a null.

For more detailed informations about Eddystone protocol check out Google's spec

Android Marshmallow Permissions

This section will briefly show how to scan beacons in Android 6.0.

In this sample we assume that in AndroidManifest.xml we use ACCESS COARSE LOCATION permission. If you need ACCESS FINE LOCATION replace it when needed.

This example uses the v7 appcompat library. But we need to transform this code into a working one on Android Marshmallow.

@Override
protected void onStart() {
    super.onStart();
    proximityManager.connect(new OnServiceReadyListener() {
      @Override
      public void onServiceReady() {
        proximityManager.startScanning();
      }
    });
}

Check Permissions

private void checkPermissionAndStart() {
    int checkSelfPermissionResult = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION);
    if (PackageManager.PERMISSION_GRANTED == checkSelfPermissionResult) {
        //already granted
        startScan();
    } else {
        if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_COARSE_LOCATION)) {
            //we should show some explanation for user here
            showExplanationDialog();
        } else {
            //request permission
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 100);
        }
    }
}

Override onRequestPermissionsResult in your Activity

@Override
 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
     if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
         if (100 == requestCode) {
             //same request code as was in request permission
             startScan();
         }

     } else {
         //not granted permission
         //show some explanation dialog that some features will not work
     }
 }

Finally start scanning

private void startScan() {
    proximityManager.connect(new OnServiceReadyListener() {
      @Override
      public void onServiceReady() {
        proximityManager.startScanning();
      }
    });
}

If everything worked you will see following dialog from Android:

Studio logging

More information about new permissions can be found in Google's Android training

Complete Example

Below you can find sample code that shows a real-life usecase of an activity that periodically scans for all iBeacons with a given name in a particular region. A SpaceListener is set to show a Toast when scanning is started and stopped.

public class MainActivity extends AppCompatActivity {

  private ProximityManager proximityManager;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    KontaktSDK.initialize("API_KEY");
    proximityManager = ProximityManagerFactory.create(this);
    configureProximityManager();
    configureListeners();
    configureSpaces();
    configureFilters();
  }

  private void configureProximityManager() {
    proximityManager.configuration()
        .scanMode(ScanMode.BALANCED)
        .scanPeriod(ScanPeriod.create(TimeUnit.SECONDS.toMillis(10), TimeUnit.SECONDS.toMillis(20)))
        .activityCheckConfiguration(ActivityCheckConfiguration.DEFAULT);
  }

  private void configureListeners() {
    proximityManager.setIBeaconListener(createIBeaconListener());
    proximityManager.setScanStatusListener(createScanStatusListener());
  }

  private void configureSpaces() {
    IBeaconRegion region = new BeaconRegion.Builder()
        .identifier("All my iBeacons")
        .proximity(UUID.fromString("123e4567-e89b-12d3-a456-426655440000"))
        .build();

    proximityManager.spaces().iBeaconRegion(region);
  }

  private void configureFilters() {
    proximityManager.filters().iBeaconFilter(IBeaconFilters.newDeviceNameFilter("JonSnow"))
  }

  @Override
  protected void onStart() {
    super.onStart();
    startScanning();
  }

  @Override
  protected void onStop() {
    proximityManager.stopScanning();
    super.onStop();
  }

  @Override
  protected void onDestroy() {
    proximityManager.disconnect();
    proximityManager = null;
    super.onDestroy();
  }

  private void startScanning() {
    proximityManager.connect(new OnServiceReadyListener() {
      @Override
      public void onServiceReady() {
        proximityManager.startScanning();
      }
    });
  }

  private void showToast(final String message) {
    runOnUiThread(new Runnable() {
      @Override
      public void run() {
        Toast.makeText(MainActivity.this, message, Toast.LENGTH_SHORT).show();
      }
    });
  }

  private IBeaconListener createIBeaconListener() {
    return new SimpleIBeaconListener() {
      @Override
      public void onIBeaconDiscovered(IBeaconDevice ibeacon, IBeaconRegion region) {
        Log.i("Sample", "IBeacon discovered! " + ibeacon.toString());
      }
    };
  }

  private ScanStatusListener createScanStatusListener() {
    return new SimpleScanStatusListener() {
      @Override
      public void onScanStart() {
        showToast("Scanning started");
      }

      @Override
      public void onScanStop() {
        showToast("Scanning stopped");
      }
    };
  }
}

Advanced Usage

Device connection for beacons with firmware v3.1 and under

Beacons Pro and Beacons with firmware 4.0 (Kontakt.io Secure) and higher are not configurable in that way. This section applies only for beacons with firmware versions below 4.0

KontaktDeviceConnection connection establishes connection with a beacon and lets you modify its characteristics. Before connection to device you must know the password needed for authorization.

Info

In order to prevent malicious users draining a battery by constantly connecting to a beacon, an unsuccessful authorization will result in switching beacon to non-connectable mode for 20 minutes. During that time device will not be accessible.

The below code shows usage of KontaktDeviceConnection to read characteristics. Always remember to close the connection after your work. Keeping connection between Android device and beacon reduces battery life of beacon. To remove possibility of keeping connection for long time, Kontakt.io beacons will disconnect from Android devices after 6 minutes.

public class ConnectActivity extends AppCompatActivity {

    private static final String TAG = ConnectActivity.class.getSimpleName();

    private KontaktDeviceConnection kontaktDeviceConnection;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //we are passing remoteBluetoothDevice and password through intent
        RemoteBluetoothDevice remoteBluetoothDevice = getIntent().getExtras().getParcelable("beacon");
        String password = getIntent().getExtras().getString("password");
        remoteBluetoothDevice.setPassword(password.getBytes());
        connect(remoteBluetoothDevice);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        disconnect();
    }

    private void connect(RemoteBluetoothDevice remoteBluetoothDevice) {
        kontaktDeviceConnection = KontaktDeviceConnectionFactory.create(this, remoteBluetoothDevice, connectionListener);
        kontaktDeviceConnection.connect();
    }

    private void disconnect() {
        if (kontaktDeviceConnection != null) {
            kontaktDeviceConnection.close();
            kontaktDeviceConnection = null;
        }
    }

    private void printCharacteristic(RemoteBluetoothDevice.Characteristics characteristics) {
        StringBuilder stringBuilder = new StringBuilder();
        String description = stringBuilder.append("proximity=").append(characteristics.getProximityUUID())
                .append("major=").append(characteristics.getMajor())
                .append("minor=").append(characteristics.getMinor())
                .append("power_level=").append(characteristics.getPowerLevel())
                .append("advertising_interval=").append(characteristics.getAdvertisingInterval())
                .append("active_profile=").append(characteristics.getActiveProfile())
                .append("model_name=").append(characteristics.getModelName())
                .append("namespace=").append(characteristics.getNamespaceId())
                .append("instanceId=").append(characteristics.getInstanceId())
                .append("url=").append(characteristics.getUrl())
                .append("manufacturer_name=").append(characteristics.getManufacturerName())
                .append("battery_level=").append(characteristics.getBatteryLevel())
                .append("firmware_version=").append(characteristics.getFirmwareRevision())
                .append("hardware_version=").append(characteristics.getHardwareRevision())
                .append("secure=").append(characteristics.isSecure())
                .toString();

        Log.d(TAG, "beacon characteristic= " + description);
    }

    private KontaktDeviceConnection.ConnectionListener connectionListener = new KontaktDeviceConnection.ConnectionListener() {
        @Override
        public void onConnected() {
            Log.d(TAG, "onConnected");
        }

        @Override
        public void onAuthenticationSuccess(RemoteBluetoothDevice.Characteristics characteristics) {
            Log.d(TAG, "onAuthenticationSuccess");
            //here you can read characteristic from device

            //do your actual work here
            printCharacteristic(characteristics);
            disconnect();
        }

        @Override
        public void onAuthenticationFailure(int failureCode) {
            switch (failureCode) {
                case KontaktDeviceConnection.FAILURE_WRONG_PASSWORD:
                    Log.d(TAG, "wrong password");
                    break;
                case KontaktDeviceConnection.FAILURE_UNKNOWN_BEACON:
                    Log.d(TAG, "unknow beacon");
                    break;
            }

            disconnect();
        }

        @Override
        public void onCharacteristicsUpdated(RemoteBluetoothDevice.Characteristics characteristics) {
            Log.d(TAG, "onCharacteristicsUpdated");
        }

        @Override
        public void onErrorOccured(int errorCode) {
            if (KontaktDeviceConnection.isGattError(errorCode)) {
                //low level bluetooth stack error. Most often 133
                int gattError = KontaktDeviceConnection.getGattError(errorCode);
                Log.d(TAG, "onErrorOccured gattError=" + gattError);
            } else {
                //sdk error
                Log.d(TAG, "onErrorOccured=" + errorCode);
            }

            disconnect();
        }

        @Override
        public void onDisconnected() {
            Log.d(TAG, "onDisconnected");
        }
    };
}

Writing to device

Changing beacon characteristics can be done in by calling overwrite method

private void write() {
    kontaktDeviceConnection.overwriteMajor(100, new WriteListener() {
        @Override
        public void onWriteSuccess(WriteResponse response) {
            disconnect();
        }

        @Override
        public void onWriteFailure(Cause cause) {
            disconnect();
        }
    });
}

Info

Writing to device will not be reflected in API. You can either send new value to API manually or use SyncableKontaktDeviceConnection

Writing batch

To write more than just one value at a time, use applyConfig.

private void writeConfig() {
    Config config = new Config.Builder()
            .major(101)
            .minor(5)
            .name("new name")
            .build();

    kontaktDeviceConnection.applyConfig(config, new WriteBatchListener<Config>() {
        @Override
        public void onWriteBatchStart(Config batchHolder) {
            //write started
        }

        @Override
        public void onWriteBatchFinish(Config batchHolder) {
            //write done, remember to disconnect!
            disconnect();
        }

        @Override
        public void onErrorOccured(int errorCode) {
            //always disconnect!
            disconnect();
        }

        @Override
        public void onWriteFailure() {
            //always disconnect!
            disconnect();
        }
    });
}

SyncableKontaktDeviceConnection

By combining KontaktDeviceConnection and KontaktCloud into a single object, we created SyncableKontaktDeviceConnection. The SyncableKontaktDeviceConnection pushes values to the bluetooth device and syncs them with its REST API representative.

public class SyncableKontaktDeviceConnectionActivity extends AppCompatActivity {

    private static final String TAG = SyncableKontaktDeviceConnectionActivity.class.getSimpleName();

    private SyncableKontaktDeviceConnection syncableKontaktDeviceConnection;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //init SDK
        KontaktSDK.initialize("your-api-key");
        //we are passing remoteBluetoothDevice and password through intent
        RemoteBluetoothDevice remoteBluetoothDevice = getIntent().getExtras().getParcelable("beacon");
        String password = getIntent().getExtras().getString("password");
        remoteBluetoothDevice.setPassword(password.getBytes());
        connect(remoteBluetoothDevice);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        disconnect();
    }

    private void disconnect() {
        if (syncableKontaktDeviceConnection != null) {
            syncableKontaktDeviceConnection.close();
            syncableKontaktDeviceConnection = null;
        }
    }

    private void connect(RemoteBluetoothDevice remoteBluetoothDevice) {
        syncableKontaktDeviceConnection = KontaktDeviceConnectionFactory.createSyncable(this, remoteBluetoothDevice, connectionListener);
        syncableKontaktDeviceConnection.connectToDevice();
    }

    private void changeAndSyncMajor() {
        syncableKontaktDeviceConnection.overwriteMajor(200, new SyncableKontaktDeviceConnection.SyncWriteListener() {
            @Override
            public void onSuccess() {
                Log.d(TAG, "written and synced");
                disconnect();
            }

            @Override
            public void onSyncFailed(ClientException e) {
                Log.d(TAG, "written but not synced");
                //here you need store current state of device and sync it to API when possible
                disconnect();
            }

            @Override
            public void onWriteFailed(WriteListener.Cause cause) {
                Log.d(TAG, "not written to beacon");
                disconnect();
            }
        });
    }

    private KontaktDeviceConnection.ConnectionListener connectionListener = new KontaktDeviceConnection.ConnectionListener() {
        @Override
        public void onConnected() {
            Log.d(TAG, "onConnected");
        }

        @Override
        public void onAuthenticationSuccess(RemoteBluetoothDevice.Characteristics characteristics) {
            changeAndSyncMajor();
        }

        @Override
        public void onAuthenticationFailure(int failureCode) {
            switch (failureCode) {
                case KontaktDeviceConnection.FAILURE_WRONG_PASSWORD:
                    Log.d(TAG, "wrong password");
                    break;
                case KontaktDeviceConnection.FAILURE_UNKNOWN_BEACON:
                    Log.d(TAG, "unknown beacon");
                    break;
            }

            disconnect();
        }

        @Override
        public void onCharacteristicsUpdated(RemoteBluetoothDevice.Characteristics characteristics) {
            Log.d(TAG, "onCharacteristicsUpdated");
        }

        @Override
        public void onErrorOccured(int errorCode) {
            if (KontaktDeviceConnection.isGattError(errorCode)) {
                //low level bluetooth stack error. Most often 133
                int gattError = KontaktDeviceConnection.getGattError(errorCode);
                Log.d(TAG, "onErrorOccured gattError=" + gattError);
            } else {
                //sdk error
                Log.d(TAG, "onErrorOccured=" + errorCode);
            }

            disconnect();
        }

        @Override
        public void onDisconnected() {

        }
    };
}

Info

If syncing with API fails, you are responsible for syncing data. Syncing may fail when network is not available, when the connection is lost during connection, and so on. You'd be wise to double check that values have syncced correctly in your app's code.

Security

The introduction of security has changed many elements of how beacons are recognized and managed. This section will describe the impact of implementing security into your application.

We can split beacon-security into two separate areas:

  1. Kontakt.io Secure Shuffling
  2. Kontakt.io Secure Communication

Info

Security was released in firmware revision 4.0 for beacons and firmware 1.0 and above for beacons pro. So if you do not upgrade your beacons to 4.0 or you don't use beacons pro you can skip this section. Beacons are NOT shuffled by default.

Shuffling

Shuffling works for iBeacon and Eddystone format. Shuffling changes following characteristics when beacon advertises data:

  • iBeacon format: Major and Minor
  • Eddystone format: instanceId

Also, following data are no longer reliable when shuffling is turned on:

  1. Device name is never transmitted
  2. MAC address changes every time when beacon shuffles its characteristics
  3. Proximity UUID and Namespace is different than when not shuffled
  4. In Eddystone only UID frame is transmitted
  5. Unique ID is never transmitted

Info

MAC address will shuffle as well on every characteristic write.

ProximityManger

ProximityManager will automatically resolve your shuffled devices and return valid information.

Info

For Beacon Pro setting secure region/namespace is mandatory! If secure region/namespace is not set for Beacon Pro's iBeacon or Eddystone it will not be resolved properly.

See Configuration section to check available ProximityManager settings for resolving shuffled devices.

Shuffled (Secure) IBeacon Regions and Eddystone Namespaces

This section is only relevant if you are using Beacon PRO devices

Below example shows how to set secure region for an iBeacon broadcasted by a Beacon Pro. Basically all you need to do is set proximity as secureProximity when creating an IBeaconRegion object. ProximityManager will automatically try to resolve secure regions before initializing a scan.

If resolving shuffled regions fails for any reason (for example during communication with cloud) onScanError callback will be invoked (see Scan Status Listener section) and scanning will NOT be started.

IBeaconRegion region = new BeaconRegion.Builder()
    .identifier("My region")
    .secureProximity(UUID.fromString("6565d504-e306-4119-8266-0f8d4401cd0a"))
    .build();

proximityManager.spaces().iBeaconRegion(region);

Setting shuffled Eddystone namespaces is analogous:

IEddystoneNamespace namespace = new EddystoneNamespace.Builder()
        .identifier("My Namespace")
        .secureNamespace("f7826da64fa24e988024")
        .build();

proximityManager.spaces().eddystoneNamespace(namespace);

Filters

Since this SDK version (3.2.1) filters can be used without any limitations, even for shuffled devices.

Kontakt.io Secure Communication

With firmware version 4.0, we encrypt communication between the beacon and any device that manages it to prevent MITM attacks eavesdropping and getting beacon connectivity passwords that are broadcast in the clear. Since we are encrypting the communications in our cloud, the only way to manage your beacon's configuration is to through our Cloud Platform.

This section will show the process of changing a characteristic with Secure Communication

The below example creates and applies a config to a device

public class MainActivity extends AppCompatActivity implements KontaktDeviceConnection.ConnectionListener {

  private KontaktCloud cloud = KontaktCloudFactory.create();
  private KontaktDeviceConnection kontaktDeviceConnection;
  private RemoteBluetoothDevice remoteBluetoothDevice;
  private Config secureConfig;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //We are passing remoteBluetoothDevice through intent
    remoteBluetoothDevice = getIntent().getExtras().getParcelable("beacon");
    prepareConfigData();
  }

  @Override
  protected void onDestroy() {
    super.onDestroy();
    close();
  }

  private void prepareConfigData() {
    //Prepare config with new values
    Config config = new Config.Builder().major(999).minor(111).txPower(4).build();

    //Create config for selected device
    cloud.configs()
        .create(config)
        .withType(DeviceType.BEACON)
        .forDevices("g0oQ")
        .execute(new CloudCallback<Config[]>() {
      @Override
      public void onSuccess(Config[] response, CloudHeaders headers) {
        fetchSecureCommunicationConfiguration();
      }

      @Override
      public void onError(CloudError error) {

      }
    });
  }

  private void fetchSecureCommunicationConfiguration() {
    cloud.configs().secure().withIds("g0oQ").execute(new CloudCallback<Configs>() {
      @Override
      public void onSuccess(Configs response, CloudHeaders headers) {
        secureConfig = response.getContent().get(0);
        writeConfig();
      }

      @Override
      public void onError(CloudError error) {
        //Ignored
      }
    });
  }

  private void writeConfig() {
    kontaktDeviceConnection = KontaktDeviceConnectionFactory.create(this, remoteBluetoothDevice, this);
    kontaktDeviceConnection.connect();
  }

  @Override
  public void onConnected() {

  }

  @Override
  public void onAuthenticationSuccess(RemoteBluetoothDevice.Characteristics characteristics) {
    //we can apply config after successful authorization
    //android ble connection should be done on main thread
    //but this callback may not be on main thread, so we redirect it to main thread
    runOnUiThread(new Runnable() {
      @Override
      public void run() {
        kontaktDeviceConnection.applySecureConfig(secureConfig.getSecureRequest(), new WriteListener() {
          @Override
          public void onWriteSuccess(WriteResponse response) {
            sendToCloud(response);
            close();
          }

          @Override
          public void onWriteFailure(Cause cause) {
            close();
          }
        });
      }
    });
  }

  @Override
  public void onAuthenticationFailure(int failureCode) {
    close();
  }

  @Override
  public void onCharacteristicsUpdated(RemoteBluetoothDevice.Characteristics characteristics) {

  }

  @Override
  public void onErrorOccured(int errorCode) {
    close();
  }

  @Override
  public void onDisconnected() {
    close();
  }

  private void sendToCloud(WriteListener.WriteResponse writeResponse) {
    //and one more step, send response to API, it removes pending configs for device if message was correct
    secureConfig.applySecureResponse(writeResponse.getExtra(), writeResponse.getUnixTimestamp());
    cloud.devices()
        .applySecureConfigs(secureConfig)
        .execute(new CloudCallback<Configs>() {
      @Override
      public void onSuccess(Configs response, CloudHeaders headers) {
        //Successfully applied secure config!
      }

      @Override
      public void onError(CloudError error) {

      }
    });
  }

  private void close() {
    if (kontaktDeviceConnection != null) {
      kontaktDeviceConnection.close();
      kontaktDeviceConnection = null;
    }
  }
}

Kontakt.io Beacon Pro Secure Communication

Communication with Beacon Pro devices is conducted the same way as in normal smart beacon case.

The only difference is creation of KontaktDeviceConnection instance itself. Providing ISecureProfile instance instead of RemoteBluetoothDevice is required:

ISecureProfile secureProfile = ...;

KontaktDeviceConnection connection = KontaktDeviceConnectionFactory.create(context, secureProfile, connectionListener);

Firmware Update (Beacon Pro)

With Beacon Pro release we introduced a firmware update feature for SDK versions 3.2.1 or higher.

This feature can only be used to update Beacon Pro devices. To update different devices (like Smart or Tough beacons) please use our Administration App.

To start firmware update process follow the example below. After successfully finishing update process, beacon will be disconnected so it can reboot and apply new firmware.

private void checkFirmware() {

KontaktCloud kontaktCloud = KontaktCloudFactory.create("API_KEY");

String uniqueId = "abcd";

//Fetch firmware info for device we want to update.
kontaktCloud.firmwares().fetch().forDevices(uniqueId).execute(new CloudCallback<Firmwares>() {
  @Override
  public void onSuccess(Firmwares response, CloudHeaders headers) {
    //If firmwares list is empty then there are no new firmware versions available for this device.
    if (!response.getContent().isEmpty()) {
      updateFirmware(response.getContent().get(0));
    }
  }

  @Override
  public void onError(CloudError error) {
    //Something went wrong
  }
});
}

private void updateFirmware(Firmware firmware) {

//Initialize connection with Secure Profile...
//When connection is set up sucessfully we can begin firmware update operation.

connection.updateFirmware(firmware, kontaktCloud, new FirmwareUpdateListener() {
  @Override
  public void onStarted() {
    Log.i("TAG", "Firmware update started.");
  }

  @Override
  public void onFinished(long totalDurationMillis) {
    //Make sure connection is closed
    connection.close()
    Log.i("TAG", "Firmware update finished. Total time: " + totalDurationMillis);
  }

  @Override
  public void onProgress(int progressPercent, String progressStatus) {
    Log.i("TAG", "Firmware update in progress: " + progressPercent + "%");
  }

  @Override
  public void onError(KontaktDfuException exception) {
    Log.e("TAG", "Something went wrong! " + exception.getMessage());
  }
});
}

The updateFirmware() method uses Kontakt Cloud instance to download and cache firmware file. The file can then be used to update other devices without downloading same data unnecessarily.

However, If you already have firmware file data stored you can use overloaded version of updateFirmware() method that accepts bytes array as a second argument and skips file download stage.