Over-The-Air Asset Deployment
For Trainer customers, Anyline provides a functionality to directly transfer & load whole use-cases “over the air” from the platform into your application at runtime and without the need to recompile.
Availability
This functionality is currently only available for the Anyline OCR and ID scanning. Fetching the view configuration over-the-air is not yet available in the Android and iOS SDK.
The basic workflow is:
1) Setup your ScanView
to fetch the view configuration of your project over-the-air to configure everything via the Trainercontext
2) Access the AssetController
of the ScanPlugin
to setup your asset update with your AssetContext
and AssetDelegate
3) Handle the workflow through the callback methods of your AssetDelegate
(e.g. check for updates, trigger update, handle errors, scan on completion)
These are the Anyline components that you need to set up in order to use this functionality:
Trainer Context
The AssetContext
is a model that must be configured with all contextual information in order to authenticate and retrieve the view configuration.
ApiKey
: This key is used to authenticate. Never store this key in clear text if you distribute your application!Project ID
: The ID of your platform project.
Asset Context
The AssetContext
is a model that must be configured with all contextual information in order to authenticate and retrieve the correct assets.
AssetContext
extends a TrainerContext
and provides additional fields:
Stage
: The stage of the assets you want to retrieve. (default: “release”)Training
: If you want to access a specific Training ID, use this property. (default: empty)
Asset Delegate
The AssetDelegate
is used to receive callbacks from the asset service in order to react in your application.
OnAssetUpdateAvailable(bool updateAvailable)
: Called with ‘true’ if assets are available.OnAssetDownloadProgress(string assetName, float progress)
: Calls back a download status about each single asset file.OnAssetUpdateError(string error)
: Called when an error occurred.OnAssetUpdateFinished(bool assetsUpdated)
: Called when the process is finished. Called with ‘true’ if assets were updated.
Asset Controller
The AssetController
provides the API to setup and handle asset updates for the ScanPlugin. The following methods give you the possibility to configure your scan plugin with your OTA assets:
SetupAssetUpdate(AssetContext context, AssetDelegate callback)
: This is the main setup point to configure the OTA deployment.CheckForUpdates()
: Checks if updates are available in the background.UpdateAssets()
: Triggers the update of the assets in background.ResetAssetUpdate()
: Resets the plugin to work with the default assets.CancelUpdate()
: Stops the asset update. This will result in receiving an error from theAssetDelegate
.
Scan Plugin
A ScanPlugin
owns an AssetController
- it uses the controller load the files from the correct location.
Examples
Code examples for the integration in native platforms can be found below:
public class MyAssetExample
{
// configures the asset service
plugin.AssetController.SetupAssetUpdate(context, callback);
// starts a background thread to check for updates
plugin.AssetController.CheckForUpdates();
// we are setting up the ScanView in an async way:
protected override async void OnNavigatedTo(NavigationEventArgs args)
{
base.OnNavigatedTo(args);
TrainerContext context = new TrainerContext();
context.ApiKey = "myApiKey";
context.ProjectID = "myProjectID";
// initialize the ScanView with the trainer context so it configures our ScanViewPlugin and ScanPlugin
await AnylineScanView.InitAsync(context, timeout: 0);
IScanViewPlugin scanViewPlugin = AnylineScanView.ScanViewPlugin as IScanViewPlugin;
scanViewPlugin.AddScanResultListener(this); // listen to results here
plugin = scanViewPlugin.ScanPlugin;
plugin.AssetController.SetupAssetUpdate(context, myAssetDelegate);
}
}
class MyAssetDelegate : IAssetDelegate
{
//this is called with the asset name and progress between 0 and 1
public void OnAssetDownloadProgress(string assetName, float progress)
{
//
}
//this is called with information about whether or not newer assets are available
public void OnAssetUpdateAvailable(bool updateAvailable)
{
// e.g. update assets here if new ones are available
if (updateAvailable) {
// this starts a background thread to receive the updates
plugin.AssetController.UpdateAssets();
} else {
// assume they are already on the device, so let's start scanning
plugin.Start();
}
}
// this is called when an error occurred at any point of the asset update/download
public void OnAssetUpdateError(string error)
{
// handle error here
// depending on your application logic, you might want to reset to the default Anyline scanner:
plugin.AssetController.ResetAssetUpdate();
}
// this is called with true if assets were updated, false otherwise
public void OnAssetUpdateFinished(bool assetsUpdated)
{
// could start scanning here if no assets were updated and no error occurred, or after assets were successfully updated
}
}
public class OTAActivity extends AppCompatActivity implements AssetDelegate
{
OcrScanPlugin scanPlugin = new OcrScanPlugin("plugin_id", licenseKey, new AnylineOcrConfig());
AssetContext context = new AssetContext();
context.setApiKey("myApiKey");
context.setProjectID("myProjectID");
// configures the asset service
scanViewPlugin.getScanPlugin().getAssetController().setupAssetUpdate(context, OTAActivity.this);
// starts a background thread to check for updates
scanViewPlugin.getScanPlugin().getAssetController().checkForUpdates();
//handle callbacks etc..
}
public class MyAssetDelegate implements AssetDelegate
{
//this is called with the asset name and progress between 0 and 1
public void onAssetDownloadProgress(String assetName, float progress)
{
// do your work here
}
//this is called when updates are available.
public void onAssetUpdateAvailable(boolean updateAvailable)
{
// e.g. update assets here if new ones are available
if (updateAvailable) {
// this starts a background thread to receive the updates
scanPlugin.updateAssets();
} else {
// assume they are already on the device, so let's start scanning
scanPlugin.Start();
}
}
// this is called when an error occurred at any point of the asset update/download
public void onAssetUpdateError(String error)
{
// handle error here
}
// this is called with true if updates were updates, false otherwise
public void onAssetUpdateFinished(boolean assetsUpdated)
{
// could start scanning here if no assets were updated and no error occurred, or after assets were successfully updated
}
}
#import "ALAssetDeploymentViewController.h"
#import <Anyline/Anyline.h>
@interface ALAssetDeploymentViewController () <ALOCRScanPluginDelegate,ALAssetDelegate,ALIDPluginDelegate>
@property ALAbstractScanPlugin *scanPlugin;
@property ALAbstractScanViewPlugin *scanViewPlugin;
@property ALScanView *scanView;
@property UILabel *projectNameLabel;
@property UILabel *projectIDLabel;
@property UIBarButtonItem *clearAssetsButton;
@property UIButton *updateAssetsButton;
@property ALAssetContext *assetContext;
@property NSString *apiKey;
@property NSString *projectID;
@property NSString *projectTitle;
//@property ALScanViewPluginConfig *viewPluginConfig;
@property NSDictionary *viewPluginConfig;
@property ALCameraConfig *cameraConfig;
@property ALFlashButtonConfig *flashButtonConfig;
@property UIColor *backgroundColor;
@property (nonatomic) BOOL updatingScripts;
@end
@implementation ALAssetDeploymentViewController
- (instancetype)initWithAPIKey:(NSString *)apiKey projectID:(NSString *)projectID projectTitle:(NSString *)projectTitle viewConfig:(NSDictionary *)viewConfig cameraConfig:(ALCameraConfig *)cameraConfig flashButtonConfig:(ALFlashButtonConfig *)flashButtonConfig {
self = [super init];
self.apiKey = apiKey;
self.projectID = projectID;
self.projectTitle = projectTitle;
self.viewPluginConfig = viewConfig;
self.cameraConfig = cameraConfig;
self.flashButtonConfig = flashButtonConfig;
return self;
}
- (void)setUpdatingScripts:(BOOL)updatingScripts {
_updatingScripts = updatingScripts;
self.updateAssetsButton.enabled = !updatingScripts;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.8];
NSError *licenseError = nil;
//Initialize the Anyline License
[AnylineSDK setupWithLicenseKey:@"ADD YOUR LICENSEKEY STRING" error:&licenseError];
if (licenseError) {
//Handle errors here
}
// Do any additional setup after loading the view.
self.assetContext = [[ALAssetContext alloc] init];
self.assetContext.apiKey = self.apiKey;
self.assetContext.projectID = self.projectID;
NSError *error;
if (error) {
[self showAlertWithTitle:@"Error creating plugin" message:error.localizedDescription];
return;
}
self.scanViewPlugin = (ALIDScanViewPlugin *)[ALAbstractScanViewPlugin scanViewPluginForConfigDict:self.viewPluginConfig delegate:self error:&error];
if ([self.scanViewPlugin isKindOfClass:ALIDScanViewPlugin.class]) {
self.scanPlugin = ((ALIDScanViewPlugin *)self.scanViewPlugin).idScanPlugin;
} else if ([self.scanViewPlugin isKindOfClass:ALOCRScanViewPlugin.class]) {
self.scanPlugin = ((ALOCRScanViewPlugin *)self.scanViewPlugin).ocrScanPlugin;
} else {
[self showAlertWithTitle:@"Error creating plugin" message:@"The project is configured for a type of plugin that the Anyline Platform demo is not set up to run."];
}
self.scanView = [[ALScanView alloc] initWithFrame:[self scanViewFrame] scanViewPlugin:self.scanViewPlugin cameraConfig:self.cameraConfig flashButtonConfig:self.flashButtonConfig];
[self.view addSubview:self.scanView];
[self.scanView startCamera];
int margin = 10;
self.projectNameLabel = [[UILabel alloc] init];
self.projectNameLabel.text = self.projectTitle;
self.projectNameLabel.adjustsFontSizeToFitWidth = YES;
self.projectNameLabel.numberOfLines = 1;
self.projectNameLabel.textColor = UIColor.whiteColor;
[self.scanView addSubview:self.projectNameLabel];
//todo: change these constraints (and the position of the 'Check for Updates' button and other UI) based on the location of the flash button specified in the flash config
NSLayoutXAxisAnchor *leftAnchor = (self.scanView.flashButton ? self.scanView.flashButton.rightAnchor : self.scanView.leftAnchor);
[self.projectNameLabel.leftAnchor constraintEqualToAnchor:leftAnchor constant:margin].active = YES;
[self.projectNameLabel.rightAnchor constraintEqualToAnchor:self.scanView.rightAnchor constant:-margin].active = YES;
[self.projectNameLabel.topAnchor constraintEqualToAnchor:self.scanView.topAnchor constant:margin].active = YES;
self.projectNameLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.projectIDLabel = [[UILabel alloc] init];
self.projectIDLabel.text = self.projectID;
self.projectIDLabel.adjustsFontSizeToFitWidth = YES;
self.projectIDLabel.numberOfLines = 1;
self.projectIDLabel.textColor = UIColor.whiteColor;
[self.scanView addSubview:self.projectIDLabel];
[self.projectIDLabel.leftAnchor constraintEqualToAnchor:leftAnchor constant:margin].active = YES;
[self.projectIDLabel.rightAnchor constraintEqualToAnchor:self.scanView.rightAnchor constant:-margin].active = YES;
[self.projectIDLabel.topAnchor constraintEqualToAnchor:self.projectNameLabel.bottomAnchor constant:margin].active = YES;
self.projectIDLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.clearAssetsButton = [[UIBarButtonItem alloc] initWithTitle:@"Log out" style:UIBarButtonItemStylePlain target:self action:@selector(resetAssets:)];
self.navigationController.navigationBar.topItem.rightBarButtonItem = self.clearAssetsButton;
self.updateAssetsButton = [UIButton buttonWithType:UIButtonTypeSystem];
[self.updateAssetsButton setTitle:@"Check for Updates" forState:UIControlStateNormal];
[self.updateAssetsButton setTitle:@"Checking…" forState:UIControlStateDisabled];
[self.updateAssetsButton setTitle:@"Checking…" forState:UIControlStateDisabled|UIControlStateHighlighted];
self.updateAssetsButton.contentEdgeInsets = UIEdgeInsetsMake(5, 5, 5, 5);
[self.updateAssetsButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal];
[self.updateAssetsButton setTitleColor:UIColor.whiteColor forState:UIControlStateDisabled];
[self.updateAssetsButton setTitleColor:UIColor.whiteColor forState:UIControlStateDisabled|UIControlStateHighlighted];
[self.updateAssetsButton sizeToFit];
self.updateAssetsButton.backgroundColor = [[UIColor alloc] initWithRed:26.0/255 green:124.0/255 blue:189.0/255 alpha:1];
[self.updateAssetsButton addTarget:self
action:@selector(updateScripts:)
forControlEvents:UIControlEventTouchUpInside];
CGFloat yCoordinate = CGRectGetMaxY(self.scanView.bounds)-20-self.updateAssetsButton.frame.size.height;
self.updateAssetsButton.frame = CGRectMake(20,yCoordinate, self.updateAssetsButton.frame.size.width, self.updateAssetsButton.frame.size.height);
[self.scanView addSubview:self.updateAssetsButton];
[self updateScripts:nil];
}
- (void)startPluginIfNotRunning {
//we start updating the scripts in viewDidLoad, and we don't want to start the plugin in viewDidAppear if the update hasn't finished yet, so we check updatingScripts here.
if (!self.scanPlugin.isRunning && !self.updatingScripts) {
[self startPlugin];
}
}
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self startPluginIfNotRunning];
}
- (CGRect)scanViewFrame {
CGRect frame = [[UIScreen mainScreen] bounds];
frame = CGRectMake(frame.origin.x, frame.origin.y + CGRectGetMaxY(self.navigationController.navigationBar.frame), frame.size.width, frame.size.height - CGRectGetMaxY(self.navigationController.navigationBar.frame));
return frame;
}
- (IBAction)updateScripts:(id)sender {
self.updatingScripts = YES;
[self stopPlugin];
if (sender == nil) {
//this is a hack for now, to reset the scripts the first time this view opens, because the core no longer renames scripts, so we have to set the correct names for them in assetDownloadProgressWithAssetName, but after the app restarts, we already have downloaded the assets, and don't know what the names should be set to. So for now resetAssetUpdate actually removes the files so they will be redownloaded.
//[self.scanPlugin resetAssetUpdate];
}
[self.scanPlugin setupAssetUpdateWithContext:self.assetContext delegate:self];
[self.scanPlugin.assetController checkForUpdates];
}
- (IBAction)resetAssets:(id)sender {
[self stopPlugin];
[self.scanPlugin.assetController resetAssetUpdate];
}
- (void)startPlugin {
NSError *error = nil;
//there can be an error the first time we start the update, if we haven't downloaded any scripts yet, but we should be able to start it in assetUpdateFinished
[self.scanViewPlugin startAndReturnError:&error];
if (error) {
[self showAlertWithTitle:@"Error starting plugin" message:error.localizedDescription];
}
}
- (void)stopPlugin {
NSError *error = nil;
[self.scanViewPlugin stopAndReturnError:&error];
NSLog(@"stopped plugin with error: %@", error);
}
#pragma mark - Anyline Result Delegates
- (void)anylineOCRScanPlugin:(ALOCRScanPlugin * _Nonnull)anylineOCRScanPlugin didFindResult:(ALOCRResult * _Nonnull)result {
NSLog(@"got result: %@",result.result);
//open another screen
[self stopPlugin];
//handle result here
}
- (void)anylineIDScanPlugin:(ALIDScanPlugin * _Nonnull)anylineIDScanPlugin didFindResult:(ALIDResult * _Nonnull)scanResult {
[self.scanViewPlugin stopAndReturnError:nil];
//Handle ID results here (cast to ALMRZIdentification, ALDrivingLicenseIdentification, ALGermanIDFrontIdentification)
}
#pragma mark - Anyline Asset Delegate
- (void)assetUpdateAvailable:(BOOL)updateAvailable {
NSLog(@"assetUpdateAvailable:%@",updateAvailable?@"YES":@"NO");
[self.updateAssetsButton setTitle:@"Check for Updates" forState:UIControlStateNormal];
if (updateAvailable) {
[self stopPlugin];
} else {
//todo: set the button title to this briefly
//self.result.text = @"No updates available";
self.updatingScripts = NO;
[self startPluginIfNotRunning];
}
}
- (void)assetUpdateFinished:(BOOL)assetsUpdated {
NSLog(@"assetUpdateFinished:%@",assetsUpdated?@"YES":@"NO");
self.updatingScripts = NO;
[self startPlugin];
}
- (void)assetUpdateError:(NSString *)error {
//Handle errors here
}
- (void)assetDownloadProgressWithAssetName:(NSString *)assetName progress:(float)progress {
//Visiualize asset download here
}
@end
Need Help?
If there’s any open question or any obstacles, please don’t hesitate to reach out to us via support@anyline.com!