{"id":19592303,"url":"https://github.com/smikhalevski/racehorse","last_synced_at":"2025-04-27T14:33:44.061Z","repository":{"id":68734596,"uuid":"556915160","full_name":"smikhalevski/racehorse","owner":"smikhalevski","description":"🐴 The bootstrapper for WebView-based Android apps.","archived":false,"fork":false,"pushed_at":"2025-03-24T07:42:05.000Z","size":11672,"stargazers_count":4,"open_issues_count":1,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-24T10:02:20.583Z","etag":null,"topics":["android","web","webview"],"latest_commit_sha":null,"homepage":"https://smikhalevski.github.io/racehorse/","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/smikhalevski.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-10-24T18:57:33.000Z","updated_at":"2025-03-24T07:42:09.000Z","dependencies_parsed_at":"2023-11-07T13:33:06.830Z","dependency_job_id":"aa04b49b-ae70-4ac4-b438-c571ab7822be","html_url":"https://github.com/smikhalevski/racehorse","commit_stats":{"total_commits":194,"total_committers":1,"mean_commits":194.0,"dds":0.0,"last_synced_commit":"0104afd660b0e928600f9f1245465df9f2b9c2a7"},"previous_names":[],"tags_count":73,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smikhalevski%2Fracehorse","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smikhalevski%2Fracehorse/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smikhalevski%2Fracehorse/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smikhalevski%2Fracehorse/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/smikhalevski","download_url":"https://codeload.github.com/smikhalevski/racehorse/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251154708,"owners_count":21544546,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","web","webview"],"created_at":"2024-11-11T08:34:34.313Z","updated_at":"2025-04-27T14:33:39.026Z","avatar_url":"https://github.com/smikhalevski.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./assets/racehorse.png\" alt=\"Racehorse\" width=\"500\"/\u003e\n\u003c/p\u003e\n\nThe bootstrapper for WebView-based Android apps.\n\n🚀\u0026ensp;**Features**\n\n- [Overview](#overview)\n- [Example app](#example-app)\n- [Request-response event chains](#request-response-event-chains)\n- [Event subscriptions](#event-subscriptions)\n- [WebView events](#webview-events)\n- [Check supported events](#check-supported-events)\n- [Proguard](#proguard)\n\n🔌\u0026ensp;**Plugins**\n\n- [Activity](#activity-plugin)\n- [Asset loader](#asset-loader-plugin)\n- [Biometric](#biometric-plugin)\n- [Biometric encrypted storage](#biometric-encrypted-storage-plugin)\n- [Contacts](#contacts-plugin)\n- [Deep link](#deep-link-plugin)\n- [Device](#device-plugin)\n- [Downloads](#download-plugin)\n- [Encrypted storage](#encrypted-storage-plugin)\n- [Evergreen](#evergreen-plugin)\n- [Facebook Login](#facebook-login-plugin)\n- [Facebook Share](#facebook-share-plugin)\n- [File chooser](#file-chooser-plugin)\n- [File system](#file-system-plugin)\n- [Google Pay](#google-pay-plugin)\n- [Google Play referrer](#google-play-referrer-plugin)\n- [Google Sign-In](#google-sign-in-plugin)\n- [HTTPS](#https-plugin)\n- [Keyboard](#keyboard-plugin)\n- [Network](#network-plugin)\n- [Notifications](#notifications-plugin)\n- [Permissions](#permissions-plugin)\n\n🍪\u0026ensp;**Cookbook**\n\n- [Blur preview on recent apps screen](#blur-preview-on-recent-apps-screen)\n\n# Overview\n\nRacehorse is the pluggable bridge that marshals events between the web app and the native Android app. To showcase how\nRacehorse works, let's create a plugin that would display\n[an Android-native toast](https://developer.android.com/guide/topics/ui/notifiers/toasts) when the web app requests it.\n\nLet's start by adding required Racehorse dependencies. In your Android project add:\n\n```kotlin\ndependencies {\n    implementation(\"org.greenrobot:eventbus:3.3.1\")\n    implementation(\"com.google.code.gson:gson:2.10.1\")\n    implementation(\"org.racehorse:racehorse:1.7.2\")\n}\n```\n\nInstall web dependencies:\n\n```shell\nnpm install racehorce\n```\n\nIf you're planning to use React, consider a Racehorse React integration package:\n\n```shell\nnpm install @racehorse/react\n```\n\nIn Android app, create a WebView:\n\n```kotlin\nimport android.webkit.WebView\n\nval webView = WebView(activity)\n// or\n// val webView = activity.findViewById\u003cWebView\u003e(R.id.web_view)\n```\n\nCreate an\n[`EventBridge`](https://smikhalevski.github.io/racehorse/android/racehorse/org.racehorse/-event-bridge/index.html)\ninstance that would be responsible for event marshalling:\n\n```kotlin\nimport org.racehorse.EventBridge\n\nval eventBridge = EventBridge(webView).apply { enable() }\n```\n\nRacehorse uses a [Greenrobot EventBus](https://greenrobot.org/eventbus) to deliver events to subscribers, so bridge must\nbe registered in the event bus:\n\n```kotlin\nimport org.greenrobot.eventbus.EventBus\n\nEventBus.getDefault().register(eventBridge)\n```\n\nHere's an event that is posted from the web to Android through the bridge:\n\n```kotlin\npackage com.example\n\nimport org.racehorse.WebEvent\n\nclass ShowToastEvent(val message: String) : WebEvent\n```\n\nNote that `ShowToastEvent` implements\n[`WebEvent`](https://smikhalevski.github.io/racehorse/android/racehorse/org.racehorse/-web-event/index.html) marker\ninterface. This is the baseline requirement to which events must conform to support marshalling from the web app to\nAndroid.\n\nNow let's add an event subscriber that would receive incoming `ShowToastEvent` and display a toast:\n\n```kotlin\npackage com.example\n\nimport android.content.Context\nimport android.widget.Toast\nimport org.greenrobot.eventbus.Subscribe\n\nclass ToastPlugin(val context: Context) {\n\n    @Subscribe\n    fun onShowToast(event: ShowToastEvent) {\n        Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()\n    }\n}\n```\n\nRegister `ToastPlugin` in the event bus, so to enable event subscriptions:\n\n```kotlin\nEventBus.getDefault().register(ToastPlugin(activity))\n```\n\nNow the native part is set up, and we can send an event from the web app:\n\n```js\nimport { eventBridge } from 'racehorse';\n\neventBridge.requestAsync({\n  // 🟡 The event class name\n  type: 'com.example.ShowToastEvent',\n  payload: {\n    message: 'Hello, world!'\n  }\n});\n```\n\nThe last step is to load the web app into the WebView. You can do this in any way that fits your needs, Racehorse\ndoesn't restrict this process in any way. For example, if your web app is running on your local machine on the port\n1234, then you can load the web app in the WebView using this snippet:\n\n```kotlin\nwebView.loadUrl(\"https://10.0.2.2:1234\")\n```\n\n# Example app\n\nThe example app consists of two parts: [the web app](./web/example) and [the Android app](./android/example). To launch\nthe app in the emulator follow the steps below.\n\nClone this repo:\n\n```shell\ngit clone git@github.com:smikhalevski/racehorse.git\ncd racehorse\n```\n\nInstall packages and build Racehorse packages and the example app:\n\n```shell\nnpm ci\nnpm run build\n```\n\nStart the web server that would serve the app for the debug build:\n\n```shell\ncd web/example\nnpm start\n```\n\nOpen `\u003cracehorse\u003e/android` in Android Studio and run `example` app.\n\n# Request-response event chains\n\nIn the [Overview](#overview) section we used an event that extends a\n[`WebEvent`](https://smikhalevski.github.io/racehorse/android/racehorse/org.racehorse/-web-event/index.html) interface.\nSuch events don't imply the response. To create a request-response chain at least two events are required:\n\n```kotlin\npackage com.example\n\nimport android.os.Build\nimport org.greenrobot.eventbus.Subscribe\nimport org.racehorse.RequestEvent\nimport org.racehorse.ResponseEvent\n\nclass GetDeviceModelRequestEvent : RequestEvent()\n\nclass GetDeviceModelResponseEvent(val deviceModel: String) : ResponseEvent()\n\nclass DeviceModelPlugin {\n\n    @Subscribe\n    fun onGetDeviceModel(event: GetDeviceModelRequestEvent) {\n        event.respond(GetDeviceModelResponseEvent(Build.MODEL))\n    }\n}\n```\n\nRequest and response events are instances of\n[`ChainableEvent`](https://smikhalevski.github.io/racehorse/android/racehorse/org.racehorse/-chainable-event/index.html).\nEvents in the chain share the same `requestId`. When a\n[`ResponseEvent`](https://smikhalevski.github.io/racehorse/android/racehorse/org.racehorse/-response-event/index.html)\nis posted to the event bus it is marshalled to the web app and resolves a promise returned from the\n[`eventBridge.requestAsync`](https://smikhalevski.github.io/racehorse/interfaces/racehorse.EventBridge.html#requestAsync):\n\n```ts\nimport { eventBridge } from 'racehorse';\n\nconst deviceModel = await eventBridge\n  .requestAsync({ type: 'com.example.GetDeviceModelRequestEvent' })\n  .then(event =\u003e event.payload.deviceModel)\n```\n\nIf an exception is thrown in `DeviceModelPlugin.onGetDeviceModel`, then promise is _rejected_ with an\n[`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) instance.\n\n## Synchronous requests\n\nIf all events in the event chain are handled on\n[the posting thread](https://greenrobot.org/eventbus/documentation/delivery-threads-threadmode/) on the Android side,\nthen a request can be handled synchronously on the web side. In the `DeviceModelPlugin` example `onGetDeviceModel` is\ncalled on the posting thread, since we didn't specify a thread mode for\n[`@Subscribe`](https://github.com/greenrobot/EventBus#eventbus-in-3-steps) annotation. So this allows web to perform a\nsynchronous request:\n\n```ts\nimport { eventBridge } from 'racehorse';\n\nconst { deviceModel } = eventBridge\n  .request({ type: 'com.example.GetDeviceModelRequestEvent' })\n  .payload;\n```\n\nIf your app initializes an event bridge after the WebView was created, you may need to establish the connection\nmanually before using synchronous requests:\n\n```ts\nawait eventBridge.connect();\n```\n\n# Event subscriptions\n\nWhile the web app can post a request event to the Android, it is frequently required that the Android would post an\nevent to the web app without an explicit request. This can be achieved using subscriptions.\n\nLet's define an event that the Android can post to the web:\n\n```kotlin\npackage com.example\n\nimport org.racehorse.NoticeEvent\n\nclass BatteryLowEvent : NoticeEvent\n```\n\nTo receive this event in the web app, add a listener:\n\n```js\nimport { eventBridge } from 'racehorse';\n\neventBridge.subscribe(event =\u003e {\n  if (event.type === 'com.example.BatteryLowEvent') {\n    // Handle the event here\n  }\n});\n```\n\nTo subscribe to an event of the given type, you can use a shortcut:\n\n```js\nimport { eventBridge } from 'racehorse';\n\neventBridge.subscribe('com.example.BatteryLowEvent', payload =\u003e {\n  // Handle the event payload here\n});\n```\n\nIf you have [an `EventBridge` registered](#overview) in the event bus, then you can post `BatteryLowEvent` event from\nanywhere in your Android app, and it would be delivered to a subscriber in the web app:\n\n```kotlin\nEventBus.getDefault().post(BatteryLowEvent())\n```\n\n# WebView events\n\nRacehorse provides clients for the WebView which post WebView-related events to the event bus, so you can subscribe to\nthem in your plugins. To init clients just set them to the WebView instance:\n\n```kotlin\nimport org.racehorse.webview.RacehorseWebChromeClient\nimport org.racehorse.webview.RacehorseWebViewClient\n\nwebView.webChromeClient = RacehorseWebChromeClient()\nwebView.webViewClient = RacehorseWebViewClient()\n```\n\nNow you can subscribe to\n[all events that a WebView instance posts](https://smikhalevski.github.io/racehorse/android/racehorse/org.racehorse.webview/index.html):\n\n```kotlin\nimport org.greenrobot.eventbus.Subscribe\nimport org.racehorse.webview.ConsoleMessageEvent\n\nclass MyPlugin {\n\n    @Subscribe\n    fun onConsoleMessage(event: ConsoleMessageEvent) {\n        // Handle the event here\n    }\n}\n\nEventBus.getDefault().register(MyPlugin())\n```\n\n# Check supported events\n\nThe web app can check that the event in supported by the Android binary. For example, to check that the app supports\nGooglePay card tokenization, you can use:\n\n```ts\nimport { eventBridge } from 'racehorse';\n\neventBridge.isSupported('org.racehorse.GooglePayTokenizeEvent');\n// ⮕ true\n```\n\n# Proguard\n\n`org.racehorse:racehorse` is an Android library (AAR) that provides its own\n[proguard rules](./android/racehorse/proguard-rules.pro), so no additional action is needed. Proguard rules prevent\nobfuscation of events and related classes which are available in Racehorse.\n\nFor example, this class and its members won't be minified:\n\n```kotlin\nclass ShowToastEvent(val message: String) : WebEvent\n```\n\n# Activity plugin\n\n[`ActivityManager`](https://smikhalevski.github.io/racehorse/interfaces/racehorse.ActivityManager.html) starts\nactivities and provides info about the activity that renders the WebView.\n\nAdd Lifecycle dependency to your Android app:\n\n```kotlin\ndependencies {\n    implementation(\"androidx.lifecycle:lifecycle-process:2.8.5\")\n}\n```\n\nInitialize the plugin in your Android app:\n\n```kotlin\nimport org.racehorse.ActivityPlugin\n\nEventBus.getDefault().register(ActivityPlugin().apply { enable() })\n```\n\nStart a new activity. For example, here's how to open Settings app and navigate user to the notification settings:\n\n```ts\nimport { activityManager, Intent } from 'racehorse';\n\nactivityManager.startActivity({\n  action: 'android.settings.APP_NOTIFICATION_SETTINGS',\n  flags: Intent.FLAG_ACTIVITY_NEW_TASK,\n  extras: {\n    'android.provider.extra.APP_PACKAGE': activityManager.getActivityInfo().packageName,\n  },\n});\n```\n\nSynchronously read the status of the current activity or subscribe to its changes:\n\n```ts\nimport { activityManager, ActivityState } from 'racehorse';\n\nactivityManager.getActivityState();\n// ⮕ ActivityState.BACKGROUND\n\nactivityManager.subscribe(state =\u003e {\n  // React to activity state changes\n});\n\nactivityManager.subscribe('foreground', () =\u003e {\n  // React to activity entering foreground\n});\n```\n\nIf you are using React, then refer to\n[`useActivityState`](https://smikhalevski.github.io/racehorse/functions/_racehorse_react.useActivityState.html) hook\nthat re-renders a component when activity state changes.\n\n```tsx\nimport { useActivityState } from '@racehorse/react';\n\nconst state = useActivityState();\n// ⮕ ActivityState.BACKGROUND\n```\n\n# Asset loader plugin\n\nAsset loader plugin requires [WebView events](#webview-events) to be enabled.\n\nAdd the WebKit dependency:\n\n```kotlin\ndependencies {\n    implementation(\"androidx.webkit:webkit:1.11.0\")\n}\n```\n\nLoad the static assets from a directory on the device when a particular URL is requested in the WebView:\n\n```kotlin\nimport androidx.webkit.WebViewAssetLoader\nimport org.racehorse.AssetLoaderPlugin\nimport org.racehorse.StaticPathHandler\n\nEventBus.getDefault().register(\n    AssetLoaderPlugin(activity).apply {\n        registerAssetLoader(\n            \"https://example.com\",\n            StaticPathHandler(File(activity.filesDir, \"www\"))\n        )\n    }\n)\n\nwebView.loadUrl(\"https://example.com\")\n```\n\nDuring development, if you're running a server on localhost, use `ProxyPathHandler` to serve contents to the webview:\n\n```kotlin\nAssetLoaderPlugin(activity).apply {\n    registerAssetLoader(\n        \"https://example.com\",\n        ProxyPathHandler(\"http://10.0.2.2:10001\")\n    )\n}\n```\n\n`AssetLoaderPlugin` would open URL in an external browser app it isn't handled by any of registered asset loaders. Since\nin the example above only https://example.com is handled by the asset loader, all other URLs are opened externally:\n\n```js\n// This would open a browser app and load google.com\nwindow.location.href = 'https://google.com'\n```\n\nTo disable this behaviour:\n\n```kotlin\nAssetLoaderPlugin(activity).apply {\n    isUnhandledRequestOpenedInExternalBrowser = false\n}\n```\n\n# Biometric plugin\n\n[`BiometricManager`](https://smikhalevski.github.io/racehorse/interfaces/racehorse.BiometricManager.html) provides the\nstatus of biometric support and allows to enroll for biometric auth.\n\nAdd [Biometric](https://developer.android.com/jetpack/androidx/releases/biometric#declaring_dependencies) dependency\nto your Android app:\n\n```kotlin\ndependencies {\n    implementation(\"androidx.biometric:biometric:1.2.0-alpha05\")\n}\n```\n\nInitialize the plugin in your Android app:\n\n```kotlin\nimport org.racehorse.BiometricPlugin\n\nEventBus.getDefault().register(BiometricPlugin(activity))\n```\n\nRead the biometric status or enroll biometric:\n\n```ts\nimport { biometricManager, BiometricAuthenticator } from 'racehorse';\n\nbiometricManager.getBiometricStatus([BiometricAuthenticator.BIOMETRIC_WEAK]);\n// ⮕ BiometricStatus.NONE_ENROLLED\n\nbiometricManager.enrollBiometric();\n// ⮕ Promise\u003cboolean\u003e\n```\n\n# Biometric encrypted storage plugin\n\n[`BiometricEncryptedStorageManager`](https://smikhalevski.github.io/racehorse/interfaces/racehorse.BiometricEncryptedStorageManager.html)\nenables a file-based persistence of a biometric-protected data.\n\nAdd [Biometric](https://developer.android.com/jetpack/androidx/releases/biometric#declaring_dependencies) dependency\nto your Android app:\n\n```kotlin\ndependencies {\n    implementation(\"androidx.biometric:biometric:1.2.0-alpha05\")\n}\n```\n\nInitialize the plugin in your Android app:\n\n```kotlin\nimport org.racehorse.BiometricEncryptedStoragePlugin\n\nEventBus.getDefault().register(\n    BiometricEncryptedStoragePlugin(\n        activity,\n\n        // The directory where encrypted data is stored\n        File(activity.filesDir, \"biometric_storage\")\n    )\n)\n```\n\nRead and write encrypted key-value pairs to the storage:\n\n```ts\nimport { biometricEncryptedStorageManager, BiometricAuthenticator } from 'racehorse';\n\nawait biometricEncryptedStorageManager.set('foo', 'bar', {\n  title: 'Authentication required',\n  authenticators: [BiometricAuthenticator.BIOMETRIC_STRONG],\n});\n// ⮕ true\n\nawait biometricEncryptedStorageManager.get('foo');\n// ⮕ 'bar'\n```\n\nTo allow device credential authentication, provide\n[`authenticationValidityDuration`](https://smikhalevski.github.io/racehorse/interfaces/racehorse.BiometricConfig.html#authenticationValidityDuration)\nthat is greater or equal to 0:\n\n```ts\nawait biometricEncryptedStorageManager.set('foo', 'bar', {\n  authenticators: [BiometricAuthenticator.DEVICE_CREDENTIAL],\n  authenticationValidityDuration: 0\n});\n```\n\nIf user enrolls biometric auth (for example, updates fingerprints stored on the device), then all secret keys used by\nthe biometric-encrypted storage are invalidated and values become inaccessible.\n\n```js\nif (biometricEncryptedStorageManager.has(key)) {\n  // Storage contains the key\n\n  biometricEncryptedStorageManager.get(key).then(\n    value =\u003e {\n      if (value !== null) {\n        // The value was successfully decrypted\n      } else {\n        // User authentication failed\n      }\n    },\n    error =\u003e {\n      if (error.name === 'KeyPermanentlyInvalidatedException') {\n        // Key was invaildated and cannot be decrypted anymore\n        biometricEncryptedStorageManager.delete(key)\n      }\n    }\n  )\n}\n```\n\n# Contacts plugin\n\n[`ContactsManager`](https://smikhalevski.github.io/racehorse/interfaces/racehorse.ContactsManager.html) provides access\nto contacts stored on the device.\n\nAdd contacts permission to the app manifest:\n\n```xml\n\n\u003cuses-permission android:name=\"android.permission.READ_CONTACTS\"/\u003e\n```\n\nInitialize the plugin in your Android app:\n\n```kotlin\nimport org.racehorse.ContactsPlugin\n\nEventBus.getDefault().register(ContactsPlugin(activity))\n```\n\nAsk a user to pick a contact or get contact by its ID:\n\n```ts\nimport { contactsManager } from 'racehorse';\n\ncontactsManager.pickContact();\n// ⮕ Promise\u003cContact | null\u003e\n\ncontactsManager.getContact(42);\n// ⮕ Contact | null\n```\n\n# Deep link plugin\n\n[`DeepLinkManager`](https://smikhalevski.github.io/racehorse/interfaces/racehorse.DeepLinkManager.html) provides access\nto deep links inside yor web app.\n\nInitialize the plugin in your Android app:\n\n```kotlin\nimport org.racehorse.DeepLinkPlugin\n\nEventBus.getDefault().register(DeepLinkPlugin())\n```\n\nOverride\n[`onNewIntent`](https://developer.android.com/reference/android/app/Activity#onNewIntent(android.content.Intent)) in\nthe main activity of yor app and post the deep link event:\n\n```kotlin\noverride fun onNewIntent(intent: Intent) {\n    super.onNewIntent(intent)\n\n    eventBus.post(OpenDeepLinkEvent(intent))\n}\n```\n\nSubscribe to new intents in the web app:\n\n```ts\nimport { deepLinkManager } from 'racehorse';\n\ndeepLinkManager.subscribe(intent =\u003e {\n  // Handle the deep link intent\n});\n```\n\n# Device plugin\n\n[`DeviceManager`](https://smikhalevski.github.io/racehorse/interfaces/racehorse.DeviceManager.html) provides access\nto various device settings.\n\nAdd compat library dependency, it is used for window insets acquisition:\n\n```kotlin\ndependencies {\n    implementation(\"androidx.appcompat:appcompat:1.7.0\")\n}\n```\n\nInitialize the plugin in your Android app:\n\n```kotlin\nimport org.racehorse.DevicePlugin\n\nEventBus.getDefault().register(DevicePlugin(activity))\n```\n\nSynchronously get device info, locale, or other data:\n\n```ts\nimport { deviceManager } from 'racehorse';\n\ndeviceManager.getDeviceInfo().apiLevel;\n// ⮕ 33\n\ndeviceManager.getPreferredLocales();\n// ⮕ ['en-US']\n```\n\nIf you are using React, then refer to\n[`useWindowInsets`](https://smikhalevski.github.io/racehorse/functions/_racehorse_react.useWindowInsets.html) hook\nto synchronize document paddings and window insets:\n\n```ts\nimport { useLayoutEffect } from 'react';\nimport { useWindowInsets } from '@racehorse/react';\n\nconst windowInsets = useWindowInsets();\n\nuseLayoutEffect(() =\u003e {\n  document.body.style.padding =\n    windowInsets.top + 'px ' +\n    windowInsets.right + 'px ' +\n    windowInsets.bottom + 'px ' +\n    windowInsets.left + 'px';\n}, [windowInsets]);\n```\n\n# Download plugin\n\n[`DownloadManager`](https://smikhalevski.github.io/racehorse/interfaces/racehorse.DownloadManager.html)\nallows staring and monitoring file downloads.\n\nInitialize the plugin in your Android app:\n\n```kotlin\nimport org.racehorse.DownloadPlugin\n\nEventBus.getDefault().register(DownloadPlugin(activity))\n```\n\nRead previously started downloads or start a new one:\n\n```ts\nimport { downloadManager } from 'racehorse';\n\ndownloadManager.addDownload('http://example.com/my.zip').then(id =\u003e {\n\n  downloadManager.getDownload(id);\n  // ⮕ Dowload { id: 1, status: 4, uri: 'http://example.com/my.zip' }\n});\n\ndownloadManager.getAllDownloads();\n```\n\n[`Download`](https://smikhalevski.github.io/racehorse/interfaces/racehorse.Download.html) instance carries the download\nstatus, progress, and file details.\n\nA storage permission must be added to support Android devices with API level \u003c= 29:\n\n```xml\n\n\u003cuses-permission\n    android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"\n    tools:ignore=\"ScopedStorage\"/\u003e\n```\n\n## Android 29 support\n\nOn Android 29 a `SecurityException` is thrown when calling a deprecated method\n[`DownloadManager.addCompletedDownload`](https://developer.android.com/reference/android/app/DownloadManager#addCompletedDownload(java.lang.String,%20java.lang.String,%20boolean,%20java.lang.String,%20java.lang.String,%20long,%20boolean))\nif permission `android.permission.WRITE_EXTERNAL_STORAGE` isn't granted. This method is used by Racehorse to populate\nthe list of previous downloads when a data URI is downloaded. To fix this exception\n[the legacy external storage model](https://developer.android.com/training/data-storage/use-cases#opt-out-in-production-app)\nmust be enabled in Android manifest for API level 29.\n\nCreate a resource file used for default config values `src/main/res/values/config.xml`\n\n```xml\n\n\u003cresources\u003e\n    \u003cbool name=\"request_legacy_external_storage\"\u003efalse\u003c/bool\u003e\n\u003c/resources\u003e\n```\n\nCreate a resource file that is specific for API level 29 `src/main/res/values-v29/config.xml`\n\n```xml\n\n\u003cresources\u003e\n    \u003cbool name=\"request_legacy_external_storage\"\u003etrue\u003c/bool\u003e\n\u003c/resources\u003e\n```\n\nConfigure the legacy external storage setting in Android manifest file:\n\n```xml\n\n\u003capplication\n    android:requestLegacyExternalStorage=\"@bool/request_legacy_external_storage\"\n/\u003e\n```\n\n## Downloadable links\n\nDownloadable links have a [`download`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a) attribute:\n\n```html\n\u003ca\n  href=\"data:image/gif;base64,R0lGODlhBwAGAJEAAAAAAP////RDNv///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQFAAADACwAAAAABwAGAAACCpxkeMudOyKMkhYAOw==\"\n  download\n\u003e\n  Download image\n\u003c/a\u003e\n```\n\nInitialize the\n[`DownloadPlugin`](https://smikhalevski.github.io/racehorse/android/racehorse/org.racehorse/-download-plugin/index.html)\nas described in the previous section, and add a Racehorse listener to enable automatic handling of downloadable links:\n\n```kotlin\nimport org.racehorse.webview.RacehorseDownloadListener\n\nwebView.setDownloadListener(RacehorseDownloadListener())\n```\n\n# Encrypted storage plugin\n\n[`EncryptedStorageManager`](https://smikhalevski.github.io/racehorse/interfaces/racehorse.EncryptedStorageManager.html)\nenables a file-based persistence of a password-protected data.\n\nInitialize the plugin in your Android app:\n\n```kotlin\nimport org.racehorse.EncryptedStoragePlugin\n\nEventBus.getDefault().register(\n    EncryptedStoragePlugin(\n        // The directory where encrypted data is stored\n        File(activity.filesDir, \"storage\"),\n\n        // The salt required to generate the encryption key\n        BuildConfig.APPLICATION_ID.toByteArray()\n    )\n)\n```\n\nRead and write encrypted key-value pairs to the storage:\n\n```ts\nimport { encryptedStorageManager } from 'racehorse';\n\nconst PASSWORD = '12345';\n\nawait encryptedStorageManager.set('foo', 'bar', PASSWORD);\n// ⮕ true\n\nawait encryptedStorageManager.get('foo', PASSWORD);\n// ⮕ 'bar'\n```\n\n# Evergreen plugin\n\n[`EvergreenManager`](https://smikhalevski.github.io/racehorse/interfaces/racehorse.EvergreenManager.html) provides a\nway to update your app using an archive that is downloadable from your server.\n\nYou can find an extensive demo of evergreen plugin usage [in the example app.](#example-app)\n\nInit the plugin and start the update download process:\n\n```kotlin\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nimport org.racehorse.evergreen.EvergreenPlugin\n\nclass MyActivity : AppCompatActivity() {\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        val evergreenPlugin = EvergreenPlugin(File(filesDir, \"app\"))\n\n        EventBus.getDefault().register(evergreenPlugin)\n\n        Thread {\n            // 🟡 Start the update process\n            evergreenPlugin.start(version = \"1.0.0\", updateMode = UpdateMode.MANDATORY) {\n                URL(\"http://example.com/bundle.zip\").openConnection()\n            }\n        }.start()\n    }\n}\n```\n\nThe snipped above would download `bundle.zip`, unpack it and store the assets in `\u003cfilesDir\u003e/app` directory. These\nassets would be labeled as version 1.0.0. During future app launches, the plugin would notice that it has the assets for\nversion 1.0.0 and would skip the download. If the version changes then the update bundle would be downloaded again.\n\nAfter the update is downloaded a\n[`BundleReadyEvent`](https://smikhalevski.github.io/racehorse/android/racehorse/org.racehorse.evergreen/-bundle-ready-event/index.html)\nevent is posted. You can use the [`AssetLoaderPlugin`](#asset-loader-plugin) to load resources provided by the evergreen\nplugin:\n\n```kotlin\n@Subscribe(threadMode = ThreadMode.MAIN)\nfun onBundleReady(event: BundleReadyEvent) {\n\n    EventBus.getDefault().register(\n        // Loads static assets when a particular URL is requested\n        AssetLoaderPlugin(\n            activity,\n            WebViewAssetLoader.Builder()\n                .setDomain(\"example.com\")\n                .addPathHandler(\n                    \"/\",\n                    // 🟡 Use assets provided by the evergreen plugin\n                    StaticPathHandler(event.appDir)\n                )\n                .build()\n        )\n    )\n\n    webView.loadUrl(\"https://example.com\")\n}\n```\n\nEvergreen plugin keeps track of downloaded bundles:\n\n- The master bundle contains current assets of the web app;\n- The pending update bundle contains assets that were downloaded but not yet applied as master.\n\nBelow is the diagram of events posted by the evergreen plugin.\n\n```mermaid\ngraph TD\n\nstart[\"start(version, updateMode)\"]\n--\u003e HasMasterBundle\n\nHasMasterBundle{{Has master bundle?}}\n--\u003e|Yes| IsSameVersionAsMasterBundle{{Is same version as master bundle?}}\n\nHasMasterBundle\n--\u003e|No| MandatoryUpdate\n\nIsSameVersionAsMasterBundle\n--\u003e|Yes| MainBundleReadyEvent([BundleReadyEvent])\n\nIsSameVersionAsMasterBundle\n--\u003e|No| HasPendingUpdateBundle{{Has pending update bundle?}} \n\nHasPendingUpdateBundle\n--\u003e|Yes| IsSameVersionAsPendingUpdateBundle{{Is same version as pending update bundle?}}\n\nIsSameVersionAsPendingUpdateBundle\n--\u003e|Yes| MainBundleReadyEvent\n\nIsSameVersionAsPendingUpdateBundle\n--\u003e|No| IsMandatoryUpdateMode\n\nHasPendingUpdateBundle\n--\u003e|No| IsMandatoryUpdateMode{{Is mandatory update mode?}}\n\nIsMandatoryUpdateMode\n---|No| BundleReadyEvent([\"BundleReadyEvent ¹\"])\n--\u003e OptionalUpdate\n\nIsMandatoryUpdateMode\n---\u003e|Yes| MandatoryUpdate\n\nsubgraph OptionalUpdate [Optional update]\nOptionalUpdateStartedEvent([UpdateStartedEvent])\n--\u003e OptionalUpdateProgressEvent([UpdateProgressEvent])\n--\u003e OptionalUpdateReadyEvent([UpdateReadyEvent])\nend\n\nsubgraph MandatoryUpdate [Mandatory update]\nMandatoryUpdateStartedEvent([UpdateStartedEvent])\n--\u003e MandatoryUpdateProgressEvent([UpdateProgressEvent])\n--\u003e MandatoryBundleReadyEvent([BundleReadyEvent])\nend\n```\n\n¹ The app is started with the assets from the available master bundle while the update is downloaded in the background.\n\nYou can monitor background updates and apply them as soon as they are ready:\n\n```ts\nimport { evergreenManager } from 'racehorse';\n\n// 1️⃣ Wait for the update bundle to be downloaded\nevergreenManager.subscribe('ready', () =\u003e {\n\n  // 2️⃣ Apply the update\n  evergreenManager.applyUpdate().then(() =\u003e {\n\n    // 3️⃣ Reload the web app to use the latest assets\n    window.location.reload();\n  });\n});\n```\n\n# Facebook Login plugin\n\n[`FacebookLoginManager`](https://smikhalevski.github.io/racehorse/interfaces/racehorse.FacebookLoginManager.html)\nenables Facebook Login support.\n\nGo to [developers.facebook.com](https://developers.facebook.com/docs/facebook-login/android/), register your app and\nadd the required dependencies and configurations.\n\nInitialize the Facebook SDK and register the plugin in your Android app:\n\n```kotlin\nimport com.facebook.FacebookSdk\nimport org.racehorse.FacebookLoginPlugin\n\nFacebookSdk.sdkInitialize(activity)\n\nEventBus.getDefault().register(FacebookLoginPlugin(activity))\n```\n\nRequest sign in from the web app that is loaded into the WebView:\n\n```ts\nimport { facebookLoginManager } from 'racehorse';\n\nfacebookLoginManager.logIn().then(accessToken =\u003e {\n  // The accessToken is not-null if log in succeeded\n});\n```\n\n# Facebook Share plugin\n\n[`FacebookShareManager`](https://smikhalevski.github.io/racehorse/interfaces/racehorse.FacebookShareManager.html)\nenables Facebook social sharing.\n\nGo to [developers.facebook.com](https://developers.facebook.com/docs/facebook-login/android/), register your app and\nadd the required dependencies and configurations.\n\nInitialize the Facebook SDK and register the plugin in your Android app:\n\n```kotlin\nimport com.facebook.FacebookSdk\nimport org.racehorse.FacebookSharePlugin\n\nFacebookSdk.sdkInitialize(activity)\n\nEventBus.getDefault().register(FacebookSharePlugin(activity))\n```\n\nTrigger Facebook social sharing flow:\n\n```ts\nimport { facebookShareManager } from 'racehorse';\n\nfacebookShareManager.shareLink({\n  contentUrl: 'http://example.com',\n});\n```\n\n# File chooser plugin\n\nFile chooser plugin requires [WebView events](#webview-events) to be enabled. This plugin enables file inputs in the\nweb app.\n\nFor example, if you have a file input:\n\n```html\n\u003cinput type=\"file\"\u003e\n```\n\nYou can register a plugin to make this input open a file chooser dialog:\n\n```kotlin\nimport org.racehorse.FileChooserPlugin\n\nEventBus.getDefault().register(FileChooserPlugin(activity))\n```\n\nIf you don't need camera support for file inputs, then the plugin doesn't require any additional configuration.\n\n## Enabling camera capture\n\nCamera capture requires a temporary file storage to write captured file to.\n\nDeclare a provider in your app manifest:\n\n```xml\n\n\u003cmanifest\u003e\n    \u003capplication\u003e\n        \u003cprovider\n            android:name=\"androidx.core.content.FileProvider\"\n            android:authorities=\"${applicationId}.provider\"\n            android:exported=\"false\"\n            android:grantUriPermissions=\"true\"\u003e\n            \u003cmeta-data\n                android:name=\"android.support.FILE_PROVIDER_PATHS\"\n                android:resource=\"@xml/file_paths\"/\u003e\n        \u003c/provider\u003e\n    \u003c/application\u003e\n\u003c/manifest\u003e\n```\n\nAdd a provider paths descriptor to XML resources, for example to `src/main/res/xml/file_paths.xml`:\n\n```xml\n\u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e\n\u003cpaths\u003e\n    \u003ccache-path name=\"cacheDir\" path=\"/\"/\u003e\n\u003c/paths\u003e\n```\n\nInitialize the plugin in your Android app, and provide the authority of the provider you've just created and the\npath that you've defined in the descriptor:\n\n```kotlin\nimport org.racehorse.FileChooserPlugin\nimport org.racehorse.TempCameraFileFactory\n\nEventBus.getDefault().register(\n    FileChooserPlugin(\n        activity,\n\n        TempCameraFileFactory(\n            activity,\n            activity.cacheDir,\n            BuildConfig.APPLICATION_ID + \".provider\"\n        )\n    )\n)\n```\n\nIf you want to store images and videos in the gallery app after they were captured through file chooser, use\n[`GalleryCameraFileFactory`](https://smikhalevski.github.io/racehorse/android/racehorse/org.racehorse/-gallery-camera-file-factory/index.html).\n\n# File system plugin\n\n[`FsManager`](https://smikhalevski.github.io/racehorse/interfaces/racehorse.FsManager.html) enables file system CRUD\noperations.\n\nInitialize the plugin in your Android app:\n\n```kotlin\nimport org.racehorse.FsPlugin\n\nEventBus.getDefault().register(FsPlugin(activity))\n```\n\nAccess files stored on the device from a WebView:\n\n```ts\nimport { fsManager, Directory } from 'racehorse';\n\nconst uri = fsManager.resolve(Directory.CACHE, 'temp.txt');\n\nconst file = fsManager.File(uri);\n\nawait file.writeText('Hello world!');\n\nawait file.readDataUri();\n// ⮕ 'data:text/plain;base64,SGVsbG8gd29ybGQh'\n```\n\n## Serving local files\n\nTo load an arbitrary file from the web view,\nuse [`localUrl`](https://smikhalevski.github.io/racehorse/classes/racehorse.File.html#localUrl):\n\n```ts\nimport { contactsManager, fsManager } from 'racehorse';\n\nconst contact = await contactsManager.pickContact();\n\nconst photoUrl = fsManager.File(contact.photoUri).localUrl;\n// ⮕ 'https://racehorce.local/fs?uri=…'\n```\n\nThe local URL can be used as a source for an image or an iframe:\n\n```ts\ndocument.getElementsByTagName('img')[0].src = photoUrl;\n```\n\n# Google Pay plugin\n\n[`GooglePayManager`](https://smikhalevski.github.io/racehorse/interfaces/racehorse.GooglePayManager.html) enables\n[Android Push Provisioning](https://developers.google.com/pay/issuers/apis/push-provisioning/android) support.\n\n[Set up the development environment](https://developers.google.com/pay/issuers/apis/push-provisioning/android/setup),\nso TapAndPay SDK is available in your app.\n\nInitialize the plugin in your Android app:\n\n```kotlin\nimport org.racehorse.GoogleSignInPlugin\n\nclass MainActivity : AppCompatActivity() {\n\n    private lateinit var googlePayPlugin: GooglePayPlugin\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        googlePayPlugin = GooglePayPlugin(this)\n\n        EventBus.getDefault().register(googlePayPlugin)\n    }\n\n    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {\n        super.onActivityResult(requestCode, resultCode, data)\n\n        // 🟡 Dispatch results back to the plugin\n        googlePayPlugin.dispatchResult(requestCode, resultCode, data)\n    }\n}\n```\n\nCheck that Google Pay is supported and properly configured by retrieving the current environment:\n\n```ts\nawait googlePayManager.getEnvironment();\n// ⮕ 'production'\n```\n\nThis call may throw an `ApiException` error that provides the insight on configuration and availability issues.\n\nTo get the token info from the wallet use:\n\n```ts\nasync function getTokenInfo(lastFour: string): GooglePayTokenInfo | undefined {\n  (await googlePayManager.listTokens()).find(tokenInfo =\u003e\n    (tokenInfo.dpanLastFour === lastFour || tokenInfo.fpanLastFour === lastFour) \u0026\u0026\n    tokenInfo.tokenServiceProvider === GooglePayTokenServiceProvider.MASTERCARD\n  );\n}\n```\n\nTo tokenize a card or resume a previously aborted tokenization:\n\n```ts\nasync function tokenizeCard(lastFour: string): GooglePayTokenInfo {\n  const tokenInfo = await getTokenInfo(lastFour);\n\n  if (!tokenInfo || tokenInfo.tokenState === GooglePayTokenState.UNTOKENIZED) {\n    // 1️⃣ The card isn't tokenized\n    await googlePayManager.pushTokenize({\n      lastFour: lastFour,\n      network: GooglePayCardNetwork.MASTERCARD,\n      tokenServiceProvider: GooglePayTokenServiceProvider.MASTERCARD,\n      // opaquePaymentCard\n      // userAddress\n      // displayName\n    });\n  } else if (tokenInfo.tokenState === GooglePayTokenState.ACTIVE) {\n    // 2️⃣ Card is already tokenized\n    return tokenInfo;\n  } else {\n    // 3️⃣ Resume card tokenization (yellow path)\n    await googlePayManager.tokenize({\n      tokenId: tokenInfo.issuerTokenId,\n      network: GooglePayCardNetwork.MASTERCARD,\n      tokenServiceProvider: GooglePayTokenServiceProvider.MASTERCARD,\n      // displayName\n    });\n  }\n\n  return getTokenInfo(lastFour);\n}\n```\n\nTo open a wallet app and reveal the tokenized card use:\n\n```ts\nasync function revealCard(lastFour: string): Promise\u003cboolean\u003e {\n  const tokenInfo = await getTokenInfo(lastFour);\n\n  return tokenInfo ? googlePayManager.viewToken(tokenInfo.issuerTokenId, tokenInfo.tokenServiceProvider) : false;\n}\n```\n\n# Google Play referrer plugin\n\n[`GooglePlayReferrerManager`](https://smikhalevski.github.io/racehorse/interfaces/racehorse.GooglePlayReferrerManager.html)\nfetches the [Google Play referrer](https://developer.android.com/google/play/installreferrer/library) information.\n\nAdd Google Play referrer SDK dependency to your Android app:\n\n```kotlin\ndependencies {\n    implementation(\"com.android.installreferrer:installreferrer:2.2\")\n}\n```\n\nInitialize the plugin in your Android app:\n\n```kotlin\nimport org.racehorse.GooglePlayReferrerPlugin\n\nEventBus.getDefault().register(GooglePlayReferrerPlugin(activity))\n```\n\nRead the Google Play referrer:\n\n```ts\nimport { googlePlayReferrerManager } from 'racehorse';\n\ngooglePlayReferrerManager.getGooglePlayReferrer();\n// ⮕ Promise\u003cstring\u003e\n```\n\n# Google Sign-In plugin\n\n[`GoogleSignInManager`](https://smikhalevski.github.io/racehorse/interfaces/racehorse.GoogleSignInManager.html) enables\nGoogle Sign-In support.\n\nGo to [console.firebase.google.com](https://console.firebase.google.com), set up a new project, and configure an\nAndroid app following all instructions. Use the `applicationId` of your app and SHA-1 that is used for app signing.\nYou can use gradle to retrieve SHA-1:\n\n```shell\n./gradlew signingReport\n```\n\nGo to [Google Cloud Console](https://console.cloud.google.com/apis/credentials) for your project and add an OAuth\nclient ID for Android.\n\nAdd Google Sign-In SDK dependencies to your Android app:\n\n```kotlin\ndependencies {\n    implementation(\"com.google.android.gms:play-services-auth:21.2.0\")\n    implementation(platform(\"com.google.firebase:firebase-bom:32.1.2\"))\n}\n```\n\nRegister the plugin in your Android app:\n\n```kotlin\nimport org.racehorse.GoogleSignInPlugin\n\nEventBus.getDefault().register(GoogleSignInPlugin(activity))\n```\n\nRequest sign in from the web app that is loaded into a WebView:\n\n```ts\nimport { googleSignInManager } from 'racehorse';\n\ngoogleSignInManager.signIn().then(account =\u003e {\n  // The account is not-null if sign in succeeded\n});\n```\n\n# HTTPS plugin\n\nAsset loader plugin requires [WebView events](#webview-events) to be enabled. HTTPS plugin forces the WebView to ignore\ncertificate issues.\n\n```kotlin\nimport org.racehorse.HttpsPlugin\n\nEventBus.getDefault().register(HttpsPlugin())\n```\n\n# Keyboard plugin\n\n[`KeyboardManager`](https://smikhalevski.github.io/racehorse/interfaces/racehorse.KeyboardManager.html) toggles\nthe software keyboard and notifies about keyboard animation.\n\nInitialize the plugin in your Android app:\n\n```kotlin\nimport org.racehorse.KeyboardPlugin\n\nEventBus.getDefault().register(KeyboardPlugin(activity).apply { enable() })\n```\n\nSynchronously read the keyboard height, show or hide the keyboard:\n\n```ts\nimport { keyboardManager } from 'racehorse';\n\nkeyboardManager.showKeyboard();\n// ⮕ true\n\nkeyboardManager.getKeyboardHeight();\n// ⮕ 630\n```\n\nSubscribe to the keyboard manager to receive notifications when the keyboard animation starts:\n\n```ts\nkeyboardManager.subscribe(animation =\u003e {\n  // Handle the started animation here.\n});\n```\n\nIf you are using React, use\n[`useKeyboardAnimation`](https://smikhalevski.github.io/racehorse/functions/_racehorse_react.useKeyboardAnimation.html)\nhook to subscribe to the keyboard animation from a component:\n\n```tsx\nimport { useKeyboardAnimation } from '@racehorse/react';\n\nuseKeyboardAnimation((animation, signal) =\u003e {\n  // Signal is aborted if animation is cancelled.\n});\n```\n\nUse [`runAnimation`](https://smikhalevski.github.io/racehorse/functions/racehorse.runAnimation.html) to run\nthe animation. For example, if your\n[app is rendered edge-to-edge](https://developer.android.com/develop/ui/views/layout/edge-to-edge), you can animate\nthe bottom padding to compensate the height of the keyboard.\n\n```ts\nimport { useKeyboardAnimation, runAnimation } from '@racehorse/react';\n\nuseKeyboardAnimation((animation, signal) =\u003e {\n  // Run the animation in sync with the native keyboard animation.\n  runAnimation(\n    animation,\n    {\n      onProgress(animation, fraction, percent) {\n        const keyboardHeight = animation.startValue + (animation.endValue - animation.startValue) * fraction;\n\n        document.body.style.paddingBottom = keyboardHeight + 'px';\n      }\n    },\n    signal\n  );\n});\n```\n\nYou may also want to scroll the window to prevent the focused element from bing obscured by the keyboard.\nUse [`scrollToElement`](https://smikhalevski.github.io/racehorse/interfaces/racehorse.scrollToElement.html) to animate\nscrolling in sync with keyboard animation:\n\n```ts\nimport { useKeyboardAnimation, scrollToElement } from '@racehorse/react';\n\nuseKeyboardAnimation((animation, signal) =\u003e {\n\n  // Ensure there's an active element to scroll to.\n  if (document.activeElement === null \u0026\u0026 !document.hasFocus()) {\n    return;\n  }\n\n  scrollToElement(document.activeElement, {\n    // Scroll animation would have the same duration and easing as the keyboard animation.\n    animation,\n    paddingBottom: animation.endValue,\n    signal,\n  });\n});\n```\n\nCheck out [the example app](./web/example/src/App.tsx#L44) that has the real-world keyboard animation handling.\n\n\u003cbr/\u003e\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./assets/keyboard-animation.gif\" alt=\"Keyboard animation\" width=\"300\"/\u003e\n\u003c/p\u003e\n\n# Network plugin\n\n[`NetworkManager`](https://smikhalevski.github.io/racehorse/interfaces/racehorse.NetworkManager.html) enables network\nconnection monitoring support.\n\nInitialize the plugin in your Android app:\n\n```kotlin\nimport org.racehorse.NetworkPlugin\n\nval networkPlugin = NetworkPlugin(activity)\n\nEventBus.getDefault().register(networkPlugin)\n```\n\nEnable the plugin when the app resumes and disable it when the app pauses:\n\n```kotlin\nfun onResume() {\n    super.onResume()\n    networkPlugin.enable()\n}\n\nfun onPause() {\n    super.onPause()\n    networkPlugin.disable()\n}\n```\n\nSynchronously read the network connection status or subscribe to changes:\n\n```ts\nimport { networkManager } from 'racehorse';\n\nnetworkManager.getNetworkStatus().isConnected;\n// ⮕ true\n\nnetworkManager.subscribe(status =\u003e {\n  // React to network status changes\n});\n```\n\nIf you are using React, then refer to\n[`useNetworkStatus`](https://smikhalevski.github.io/racehorse/functions/_racehorse_react.useNetworkStatus.html) hook\nthat re-renders a component when network status changes.\n\n```tsx\nimport { useNetworkStatus } from '@racehorse/react';\n\nconst status = useNetworkStatus();\n\nstatus.isConnected;\n// ⮕ true\n\nstatus.type;\n// ⮕ 'wifi'\n```\n\n# Notifications plugin\n\n[`NotificationsManager`](https://smikhalevski.github.io/racehorse/interfaces/racehorse.NotificationsManager.html)\nprovides access to Android system notifications status.\n\nInitialize the plugin in your Android app:\n\n```kotlin\nimport org.racehorse.NotificationsPlugin\n\nEventBus.getDefault().register(NotificationsPlugin(activity))\n```\n\nSynchronously check that notifications are enabled:\n\n```ts\nimport { notificationsManager } from 'racehorse';\n\nnotificationsManager.areNotificationsEnabled();\n// ⮕ true\n```\n\n# Permissions plugin\n\n[`PermissionsManager`](https://smikhalevski.github.io/racehorse/interfaces/racehorse.PermissionsManager.html) allows\nchecking and requesting application permissions.\n\nInitialize the plugin in your Android app:\n\n```kotlin\nimport org.racehorse.PermissionsPlugin\n\nEventBus.getDefault().register(PermissionsPlugin(activity))\n```\n\nCheck that a permission is granted, or ask for permissions:\n\n```ts\nimport { permissionsManager } from 'racehorse';\n\npermissionsManager.isPermissionGranted('android.permission.ACCESS_WIFI_STATE');\n// ⮕ true\n\npermissionsManager.askForPermission('android.permission.CALL_PHONE');\n// ⮕ Promise\u003cboolean\u003e\n```\n\n# Cookbook\n\n## Blur preview on recent apps screen\n\nPost a custom\n[`NoticeEvent`](https://smikhalevski.github.io/racehorse/android/racehorse/org.racehorse/-notice-event/index.html) event\nin [`onWindowFocusChanged`](https://developer.android.com/reference/android/app/Activity#onWindowFocusChanged(boolean)):\n\n```kotlin\npackage com.myapplication\n\nimport org.greenrobot.eventbus.EventBus\nimport org.racehorse.NoticeEvent\n\nclass WindowFocusChangedEvent(val hasFocus: Boolean) : NoticeEvent\n\nclass MainActivity {\n\n    // Don't forget to init Racehorse here\n\n    override fun onWindowFocusChanged(hasFocus: Boolean) {\n        EventBus.getDefault().post(WindowFocusChangedEvent(hasFocus))\n    }\n}\n```\n\nIn the web app, subscribe to this event and apply the blur filter to the body:\n\n```ts\neventBridge.subscribe('com.myapplication.WindowFocusChangedEvent', payload =\u003e {\n  document.body.style.filter = payload.hasFocus ? 'none' : 'blur(30px)';\n});\n```\n\nNow your application would become blurred when it is going to background and become non-blurred when it comes to the\nforeground. \n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmikhalevski%2Fracehorse","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsmikhalevski%2Fracehorse","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmikhalevski%2Fracehorse/lists"}