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 ID | com.klug.system |
| Current version | 1.2.0 |
| Platforms | Android (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 API | One class: com.klug.system.KlugSystem |
| Delivery | com.klug.system.ane (the extension), the ActionScript library (SWC) for code completion, and this documentation |
Requirements
| AIR SDK | 51.x (HARMAN) |
| iOS | iOS 13 or later, real devices |
| Android | Android 6 or later; biometric confirmation needs Android 9 or later |
| Devices | Biometrics 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. |
| Permissions | Android: ACCESS_NETWORK_STATE, USE_BIOMETRIC, USE_FINGERPRINT · iOS: NSFaceIDUsageDescription. All are install-time declarations; no runtime permission dialog is involved. |
| Store rating | Android: the app must be installed from Google Play. iOS: App Store builds; TestFlight builds never show the dialog. |
Add the extension
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:
<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.
<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:
<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.
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().
| Android | Opens 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. |
| iOS | Presents 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. |
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. |
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.
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. |
| cancelLabel | The 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.
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.
| -1 | Authentication failed. Also used for any error without a more specific code (both platforms). |
| -2 | The user cancelled, e.g. with the cancel button (both). |
| -3 | The user chose the fallback button "Enter Password" (iOS). |
| -4 | The system cancelled, e.g. another app came to the front; on Android also after cancelAuthenticate() (both). |
| -6 | Biometrics not available on this device (both). |
| -7 | No finger or face enrolled (both). |
| -8 | Locked out after too many failed attempts (both). |
| -9 | The prompt was closed by the app: cancelAuthenticate() or a new authenticate() call (iOS). |
| other | On 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.
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.
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.
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. |
| isSupported | public function get isSupported():Boolean True where the native part is present (Android, iOS devices). |
| lastAuthError | public 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_CLOSED | public static const BROWSER_CLOSED:String = "browserClosed" Event type. |
| NETWORK_CHANGE | public static const NETWORK_CHANGE:String = "networkChange" Event type. |
| AUTH_SUCCESS | public static const AUTH_SUCCESS:String = "authSuccess" Event type. |
| AUTH_FAILED | public static const AUTH_FAILED:String = "authFailed" Event type. |
| BIOMETRY_FINGERPRINT | public static const BIOMETRY_FINGERPRINT:int = 1 Type for canAuthenticate(): Touch ID (iOS), any biometric (Android). |
| BIOMETRY_FACE | public 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_CLOSED | The 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_CHANGE | After startNetworkWatch(): usually once right after the start, then on every change of the connection. Read isReachable() and isWWAN(). |
| KlugSystem.AUTH_SUCCESS | The user was confirmed by the biometric prompt. |
| KlugSystem.AUTH_FAILED | The 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