← KlugSystem

Documentation

KlugSystem documentation

com.klug.system

In-app browser, network status, biometric confirmation and store rating on Android and iOS: setup, guides, the full API and every event and error code.

iOSAndroid Version 1.2.0 Product page · Buy

Introduction

KlugSystem bundles four system services that most mobile apps need into one small extension for Android and iOS:

  • In-app browser: web pages open in a Chrome Custom Tab (Android) or SFSafariViewController (iOS) with your toolbar colour, and your app learns when the user is back
  • Network status: online or not, Wi-Fi or mobile data, and an event when the connection changes
  • Biometric confirmation: fingerprint, Touch ID or Face ID with the system prompt, and the same error codes on both platforms
  • Store rating: the App Store or Google Play in-app rating dialog
Extension IDcom.klug.system
Current version1.2.0
PlatformsAndroid (arm, arm64, x86, x86_64), iOS (devices, arm64). On other platforms, including the iOS Simulator and desktop, isSupported is false and every call returns false or does nothing.
ActionScript APIOne class: com.klug.system.KlugSystem
Deliverycom.klug.system.ane (the extension), the ActionScript library (SWC) for code completion, and this documentation

Requirements

AIR SDK51.x (HARMAN)
iOSiOS 13 or later, real devices
AndroidAndroid 6 or later; biometric confirmation needs Android 9 or later
DevicesBiometrics need a sensor with at least one enrolled finger or face. The in-app browser on Android needs a browser app; Chrome and other browsers with Custom Tabs support show the in-app tab.
PermissionsAndroid: ACCESS_NETWORK_STATE, USE_BIOMETRIC, USE_FINGERPRINT · iOS: NSFaceIDUsageDescription. All are install-time declarations; no runtime permission dialog is involved.
Store ratingAndroid: the app must be installed from Google Play. iOS: App Store builds; TestFlight builds never show the dialog.

Add the extension

Self-contained: no core library ANE and no dependency ANEs are needed. One file, one extensionID.

1 · Copy the ANE and declare it

Copy com.klug.system.ane into your project's extension folder (the folder you pass to ADT with -extdir, or the one your IDE links as native extensions) and add the extension ID to the application descriptor:

app.xml
<extensions>
    <extensionID>com.klug.system</extensionID>
</extensions>

2 · Android manifest additions

Network status needs ACCESS_NETWORK_STATE. Biometric confirmation needs USE_BIOMETRIC, and USE_FINGERPRINT for the sensor check on Android 9. Leave out what you do not use.

app.xml
<android>
    <manifestAdditions><![CDATA[
        <manifest android:installLocation="auto">
            <!-- network status -->
            <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

            <!-- biometric confirmation -->
            <uses-permission android:name="android.permission.USE_BIOMETRIC" />
            <uses-permission android:name="android.permission.USE_FINGERPRINT" />
        </manifest>
    ]]></manifestAdditions>
</android>

The store rating on Android uses Google's Play In-App Review library. The extension declares it as a Gradle dependency, so ADT adds it automatically when it packages your Android app; the build machine needs access to Google's Maven repository for that. The in-app browser needs no manifest entries.

3 · iOS InfoAdditions

Face ID requires a usage description. Replace the text with your own wording:

app.xml
<iPhone>
    <InfoAdditions><![CDATA[
        <key>NSFaceIDUsageDescription</key>
        <string>Use Face ID to confirm that it is you.</string>
    ]]></InfoAdditions>
</iPhone>

No entitlements are needed. The in-app browser, network status and store rating need no Info.plist keys.

Getting started

Get the instance, check support, and listen for the events you need. Network and biometric checks return their answer directly; the browser, network changes and biometric results arrive as events.

Main.as
import com.klug.system.KlugSystem;
import flash.events.Event;

var sys:KlugSystem = KlugSystem.getInstance();   // singleton: never call new KlugSystem() yourself

if ( sys.isSupported )                           // Android and iOS devices
{
    trace( "online: " + sys.isReachable() + ", mobile data: " + sys.isWWAN() );

    sys.addEventListener( KlugSystem.BROWSER_CLOSED, function( e:Event ):void {
        trace( "back from the browser" );
    });
    sys.openBrowser( "https://example.com/help", 0x1A4378 );   // toolbar colour 0xRRGGBB
}

Guides

Open web pages in the in-app browser

openBrowser(url, toolbarColor) shows a web page without leaving your app. toolbarColor is an RGB value such as 0x1A4378 (default white). The method returns true when the page was opened and false when it could not be opened, so you can fall back to navigateToURL().

AndroidOpens the page as a Chrome Custom Tab with your toolbar colour and the page title. A browser without Custom Tabs support opens the page normally. The tab belongs to the browser app, so your app is in the background meanwhile. BROWSER_CLOSED fires when your app becomes active again. Returns false if no app can open the URL.
iOSPresents SFSafariViewController over your app, with your colour as the bar tint. BROWSER_CLOSED fires when the user taps Done. Use only http:// and https:// URLs; returns false if the URL cannot be parsed or has no scheme.
Help.as
import com.klug.system.KlugSystem;
import flash.events.Event;
import flash.net.URLRequest;
import flash.net.navigateToURL;

private var sys:KlugSystem = KlugSystem.getInstance();

public function openPage( url:String ):void
{
    if ( sys.isSupported && sys.openBrowser( url, 0x2B2B2B ) )
    {
        sys.addEventListener( KlugSystem.BROWSER_CLOSED, onBrowserClosed );
        pauseVideo();                                // your app function
    }
    else
    {
        navigateToURL( new URLRequest( url ) );   // system browser as fallback
    }
}

private function onBrowserClosed( e:Event ):void
{
    sys.removeEventListener( KlugSystem.BROWSER_CLOSED, onBrowserClosed );
    resumeVideo();                                   // your app function
    reloadAccountState();                            // e.g. after a payment page
}

Check the network

isReachable() and isWWAN() answer at once, without a network request:

isReachable()Android: the active network offers internet access. iOS: the system reports a route to the internet. A captive portal or a server that is down can still make real requests fail.
isWWAN()True when the active connection is mobile data (cellular). False on Wi-Fi, Ethernet and when offline.
BeforeStream.as
public function startLiveView():void
{
    if ( !sys.isReachable() )
    {
        showMessage( "You are offline." );                           // your app
        return;
    }
    if ( sys.isWWAN() && !userAllowsMobileData )                    // your app's setting
    {
        askMobileData( startStreamNow );                               // your app
        return;
    }
    startStreamNow();
}

React to network changes

startNetworkWatch() dispatches NETWORK_CHANGE whenever the connection changes. The event carries no details: read isReachable() and isWWAN() in the handler. The first event usually arrives right after the start. On Android several events can arrive in quick succession (network available, capabilities changed, previous network lost), so react to the resulting state, ideally after a short delay. Calling startNetworkWatch() twice has no extra effect; stopNetworkWatch() ends the watch.

NetworkWatch.as
import flash.utils.clearTimeout;
import flash.utils.setTimeout;

private var netTimer:uint;

public function watchNetwork():void
{
    sys.addEventListener( KlugSystem.NETWORK_CHANGE, onNetworkChange );
    sys.startNetworkWatch();
}

private function onNetworkChange( e:Event ):void
{
    clearTimeout( netTimer );
    netTimer = setTimeout( applyNetworkState, 500 );   // settle bursts of changes
}

private function applyNetworkState():void
{
    if ( !sys.isReachable() )  showOfflineBanner();       // your app functions
    else if ( sys.isWWAN() )   showMobileDataHint();
    else                       hideBanners();
}

public function unwatchNetwork():void
{
    sys.stopNetworkWatch();
    sys.removeEventListener( KlugSystem.NETWORK_CHANGE, onNetworkChange );
    clearTimeout( netTimer );
}

Confirm the user with biometrics

First ask whether a biometric method is available, then show the system prompt with authenticate(reason, cancelLabel). The result arrives as AUTH_SUCCESS or AUTH_FAILED; after a failure, lastAuthError holds the reason.

canAuthenticate(type)iOS: BIOMETRY_FINGERPRINT (1) is true on Touch ID devices, BIOMETRY_FACE (2) on Face ID devices, in both cases only with an enrolled finger or face. Android 9 and later: type 1 is true when any biometric (fingerprint, face, iris) is enrolled; type 2 is always false because Android does not tell the methods apart. Below Android 9: always false.
authenticate()iOS: the Touch ID or Face ID prompt shows reason as the explanation; the device passcode is not offered. Android: the BiometricPrompt shows reason as its title and cancelLabel (default: the system's "Cancel") as its button. Returns false on Android below 9; otherwise true, and the result follows as an event.
cancelLabelThe label of the cancel button. On iOS it replaces the system's "Cancel".

On Android a single wrong finger does not end the prompt; the user can try again until the system gives up. On iOS, after a failed attempt, the prompt can offer an "Enter Password" button; choosing it ends with error -3 so your app can show its own password entry.

ConfirmLogout.as
public function confirmLogout():void
{
    var bio:Boolean = sys.canAuthenticate( KlugSystem.BIOMETRY_FINGERPRINT )
                   || sys.canAuthenticate( KlugSystem.BIOMETRY_FACE );   // check both on iOS

    if ( !bio )
    {
        askForPassword();                                                 // your app
        return;
    }
    sys.addEventListener( KlugSystem.AUTH_SUCCESS, onAuthSuccess );
    sys.addEventListener( KlugSystem.AUTH_FAILED,  onAuthFailed );
    if ( !sys.authenticate( "Confirm to log out of this device", "Use password" ) )
    {
        removeAuthListeners();
        askForPassword();
    }
}

private function onAuthSuccess( e:Event ):void
{
    removeAuthListeners();
    logOut();                                                             // your app
}

private function onAuthFailed( e:Event ):void
{
    removeAuthListeners();
    switch ( sys.lastAuthError )
    {
        case -2:  // the user pressed the cancel button ("Use password")
        case -3:  // iOS: "Enter Password"
        case -8:  // locked out after too many attempts
            askForPassword(); break;
        default:  // -4 system cancel (e.g. a call came in), -1 failed, others
            showMessage( "Not confirmed. Please try again." );
    }
}

private function removeAuthListeners():void
{
    sys.removeEventListener( KlugSystem.AUTH_SUCCESS, onAuthSuccess );
    sys.removeEventListener( KlugSystem.AUTH_FAILED,  onAuthFailed );
}

Error codes after AUTH_FAILED

Both platforms report the error numbers of Apple's LocalAuthentication framework, so one handler covers both.

-1Authentication failed. Also used for any error without a more specific code (both platforms).
-2The user cancelled, e.g. with the cancel button (both).
-3The user chose the fallback button "Enter Password" (iOS).
-4The system cancelled, e.g. another app came to the front; on Android also after cancelAuthenticate() (both).
-6Biometrics not available on this device (both).
-7No finger or face enrolled (both).
-8Locked out after too many failed attempts (both).
-9The prompt was closed by the app: cancelAuthenticate() or a new authenticate() call (iOS).
otherOn iOS further LocalAuthentication codes can occur, e.g. -5 when no device passcode is set.

Cancel a running prompt

cancelAuthenticate() closes a prompt that is still open, for example when your screen closes or a session ends. An AUTH_FAILED may follow (-9 on iOS, -4 on Android), so remove or guard your listeners first if you do not want to react to it. Calling authenticate() while a prompt is open replaces it in the same way.

Cancel.as
public function onSessionExpired():void
{
    removeAuthListeners();      // ignore the AUTH_FAILED that may follow
    sys.cancelAuthenticate();
}

Ask for a store rating

requestReview() asks the store to show its rating dialog inside your app: SKStoreReviewController on iOS, Google Play In-App Review on Android. The store decides whether the dialog really appears, and your app is not told whether it did or whether the user rated. Apple shows it at most three times in 365 days and never in TestFlight; Google Play has its own quota and shows it only for installs from Google Play. Ask after a positive moment, never in response to a button labelled "Rate us", because the dialog may not appear.

Rating.as
import flash.net.SharedObject;

public function onSessionCompleted():void
{
    var so:SharedObject = SharedObject.getLocal( "rating" );
    so.data.sessions = int( so.data.sessions ) + 1;
    so.flush();

    // after the 10th good session, then again after the 50th
    if ( so.data.sessions == 10 || so.data.sessions == 50 )
        sys.requestReview();   // returns true when the request was passed to the store
}

To test the Android dialog, install the app through Google Play, for example from an internal test track or with internal app sharing.

App lifecycle

The network watch keeps running while your app is in the background. If you do not need updates there, stop it on Event.DEACTIVATE, start it again on Event.ACTIVATE and read the current state right away, because changes made meanwhile are not replayed. A biometric prompt that is interrupted by the system ends with AUTH_FAILED and error -4.

Lifecycle.as
import flash.desktop.NativeApplication;

NativeApplication.nativeApplication.addEventListener( Event.DEACTIVATE, function( e:Event ):void {
    sys.stopNetworkWatch();
});
NativeApplication.nativeApplication.addEventListener( Event.ACTIVATE, function( e:Event ):void {
    sys.startNetworkWatch();
    applyNetworkState();   // your handler from the guide above
});

On Android, opening the in-app browser itself sends your app to the background. Keep that in mind if you stop work on DEACTIVATE; BROWSER_CLOSED tells you when the user is back.

API reference

com.klug.system.KlugSystem

Extends flash.events.EventDispatcher. Singleton.

getInstance()public static function getInstance():KlugSystem
Returns the single instance and creates it on the first call. Calling new KlugSystem() a second time throws an Error.
isSupportedpublic function get isSupported():Boolean
True where the native part is present (Android, iOS devices).
lastAuthErrorpublic var lastAuthError:int = 0
Error code of the last AUTH_FAILED, in LocalAuthentication numbers on both platforms (see the error table). Set just before the event is dispatched; -1 if the platform gave no code.
openBrowser()public function openBrowser(url:String, toolbarColor:int = 0xFFFFFF):Boolean
Opens url in the in-app browser; toolbarColor as 0xRRGGBB. Returns true if the page was opened, false otherwise (or if unsupported). BROWSER_CLOSED follows when the user is back.
isReachable()public function isReachable():Boolean
True if the device currently has a connection to the internet. Synchronous. False if unsupported.
isWWAN()public function isWWAN():Boolean
True if the current connection is mobile data. Synchronous. False if unsupported.
startNetworkWatch()public function startNetworkWatch():void
Starts dispatching NETWORK_CHANGE. A second call while running has no effect.
stopNetworkWatch()public function stopNetworkWatch():void
Stops the network watch.
canAuthenticate()public function canAuthenticate(type:int = 1):Boolean
True if the biometric method type (BIOMETRY_FINGERPRINT or BIOMETRY_FACE) is available and enrolled. iOS: 1 = Touch ID, 2 = Face ID. Android 9+: 1 = any enrolled biometric, 2 = always false. False if unsupported.
authenticate()public function authenticate(reason:String, cancelLabel:String = ""):Boolean
Shows the biometric prompt with reason and an optional cancel button label. Returns true if the prompt was requested (result follows as AUTH_SUCCESS or AUTH_FAILED), false on Android below 9 or if unsupported.
cancelAuthenticate()public function cancelAuthenticate():void
Closes a running biometric prompt; AUTH_FAILED may follow.
requestReview()public function requestReview():Boolean
Asks the App Store or Google Play for the in-app rating dialog. Returns true if the request was passed on; whether the dialog appears is decided by the store. False if unsupported.
BROWSER_CLOSEDpublic static const BROWSER_CLOSED:String = "browserClosed"
Event type.
NETWORK_CHANGEpublic static const NETWORK_CHANGE:String = "networkChange"
Event type.
AUTH_SUCCESSpublic static const AUTH_SUCCESS:String = "authSuccess"
Event type.
AUTH_FAILEDpublic static const AUTH_FAILED:String = "authFailed"
Event type.
BIOMETRY_FINGERPRINTpublic static const BIOMETRY_FINGERPRINT:int = 1
Type for canAuthenticate(): Touch ID (iOS), any biometric (Android).
BIOMETRY_FACEpublic static const BIOMETRY_FACE:int = 2
Type for canAuthenticate(): Face ID (iOS).

Events

All events are plain flash.events.Event objects dispatched on the KlugSystem instance; the type is one of the constants below. They carry no data: read the state from the instance.

KlugSystem.BROWSER_CLOSEDThe user is back from the in-app browser. iOS: Done was tapped. Android: your app became active again after openBrowser() had sent it to the background.
KlugSystem.NETWORK_CHANGEAfter startNetworkWatch(): usually once right after the start, then on every change of the connection. Read isReachable() and isWWAN().
KlugSystem.AUTH_SUCCESSThe user was confirmed by the biometric prompt.
KlugSystem.AUTH_FAILEDThe prompt ended without confirmation. The reason is in lastAuthError.

The other methods return their result directly (isReachable(), isWWAN(), canAuthenticate()) or report no result at all (requestReview()).

Troubleshooting

canAuthenticate() is false on an iPhone with Face ID

The default type is BIOMETRY_FINGERPRINT, which on iOS means Touch ID. Check BIOMETRY_FACE as well, as in the guide. Also check that NSFaceIDUsageDescription is in your InfoAdditions; without it Face ID cannot be used.

canAuthenticate(BIOMETRY_FACE) is false on Android, although the phone has face unlock

Android does not report which biometric method is enrolled. Use BIOMETRY_FINGERPRINT: on Android it covers every enrolled biometric.

authenticate() returns false on Android

The device runs Android 8 or older. Offer your password entry instead.

I get AUTH_FAILED right after calling cancelAuthenticate() or authenticate() again

Closing a running prompt ends it with an error (-9 on iOS, -4 on Android). Remove your listeners before you cancel, or ignore these codes while you know you cancelled.

BROWSER_CLOSED fires on Android although the page is still open

On Android the tab belongs to the browser app, and "closed" means your app became active again. If the user switches back to your app with the app switcher, the event fires even though the tab still exists in the browser.

The app crashes or nothing happens when I open a custom URL scheme on iOS

The in-app browser on iOS shows only web pages. Pass http:// or https:// URLs; open other schemes with navigateToURL().

NETWORK_CHANGE fires several times for one change on Android

Android reports a new network, changed capabilities and the loss of the previous network separately. Collect the events for a moment and evaluate the state once, as in the guide.

isReachable() is true, but my request fails

Reachability describes the device's connection, not your server. Captive portals in hotels or a server outage still need normal error handling for your requests.

requestReview() returns true, but no dialog appears

The store decides. On iOS the dialog never appears in TestFlight and at most three times a year in the App Store build. On Android it appears only for installs from Google Play and within Play's quota. The return value only means that the request was passed on.

The Android build fails to resolve the review library

The Play In-App Review library is fetched through Gradle when ADT packages the app. Make sure the build machine can reach Google's Maven repository, or that the library is in the local Gradle cache.

isSupported is false on iOS

The native part is built for iOS devices. In the iOS Simulator the fallback is used. Test on a device.

Changelog

1.2.0

  • Android: requestReview() shows the Google Play In-App Review dialog

1.1.0

  • iOS: requestReview() asks for the App Store rating dialog

1.0.0

  • First release for Android and iOS: in-app browser with toolbar colour and close event, network status and change events, biometric confirmation with the same error codes on both platforms