react-native-android-overlay 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mario D.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,267 @@
1
+ # react-native-android-overlay
2
+
3
+ A lightweight and performant React Native library for Android that allows you to render registered React Native
4
+ components inside a floating system overlay window on top of other applications.
5
+
6
+ ---
7
+
8
+ ## Features
9
+
10
+ - Render React Native components as floating overlays above other apps
11
+ - Built on top of the new React Native architecture
12
+ - Automatically tracks the application activity lifecycle
13
+ - Keeps the JavaScript runtime active when the app is minimized, allowing animations, timers and JS logic to continue
14
+ running
15
+ - Supports drag and move gestures with touch interception
16
+ - Fully configurable Android foreground service notification:
17
+ - Title
18
+ - Description
19
+ - Icon
20
+ - Notification channel
21
+
22
+ ---
23
+
24
+ ## Requirements
25
+
26
+ - React Native >= 0.76
27
+ - Android >= 8.0 (API 26)
28
+ - New Architecture enabled
29
+
30
+ ---
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ npm install react-native-android-overlay
36
+ ```
37
+
38
+ or:
39
+
40
+ ```bash
41
+ yarn add react-native-android-overlay
42
+ ```
43
+
44
+ or:
45
+
46
+ ```bash
47
+ pnpm add react-native-android-overlay
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Android information
53
+
54
+ The library automatically merges the required permissions into your Android manifest:
55
+
56
+ ```xml
57
+
58
+ <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
59
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
60
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
61
+ ```
62
+
63
+ ### Permissions
64
+
65
+ - `SYSTEM_ALERT_WINDOW`
66
+ - Allows displaying overlays above other applications
67
+
68
+ - `FOREGROUND_SERVICE`
69
+ - Required to keep the overlay service alive in background
70
+
71
+ - `FOREGROUND_SERVICE_SPECIAL_USE`
72
+ - Required on Android 14+
73
+
74
+ ---
75
+
76
+ ## Foreground service
77
+
78
+ This library uses an Android foreground service to keep the React Native runtime alive while your application is
79
+ minimized.
80
+
81
+ When `foreground: true`:
82
+
83
+ - Android displays a persistent foreground service notification
84
+ - JavaScript logic continues running
85
+
86
+ > NOTE: Android requires a foreground service notification to keep the service alive.
87
+ > On Android 13+, the notification visibility depends on the user's notification permission settings.
88
+ > By default,
89
+ > the notification may not be visible unless the app has requested and been granted notification permission.
90
+
91
+ When `foreground: false`:
92
+
93
+ - Android may stop the process at any time depending on system policies
94
+
95
+ ---
96
+
97
+ # Usage
98
+
99
+ ## 1. Register your overlay component
100
+
101
+ In your root entry file (`index.js` / `index.ts`):
102
+
103
+ ```javascript
104
+ import { AppRegistry } from 'react-native';
105
+ import App from './App';
106
+ import MyOverlayComponent from './MyOverlayComponent';
107
+
108
+ AppRegistry.registerComponent(
109
+ 'MainApp',
110
+ () => App
111
+ );
112
+
113
+ // Register component used by the overlay
114
+ AppRegistry.registerComponent(
115
+ 'OverlayView',
116
+ () => MyOverlayComponent
117
+ );
118
+ ```
119
+
120
+ ---
121
+
122
+ ## 2. Start the overlay
123
+
124
+ ```typescript
125
+ import { OverlayManager } from 'react-native-android-overlay';
126
+
127
+ const startOverlay = async () => {
128
+ const hasPermission = await OverlayManager.hasPermission();
129
+
130
+ if (!hasPermission) {
131
+ OverlayManager.requestPermission();
132
+ return;
133
+ }
134
+
135
+ OverlayManager.startOverlay('OverlayView', {
136
+ width: 300,
137
+ height: 150,
138
+
139
+ x: 0,
140
+ y: 150,
141
+
142
+ gravity: 'bottom',
143
+
144
+ draggable: true,
145
+ touchable: true,
146
+ focusable: false,
147
+
148
+ foreground: true,
149
+
150
+ notificationTitle: 'Overlay Active',
151
+ notificationText: 'Overlay is running in background',
152
+ notificationIcon: 'ic_launcher',
153
+
154
+ channelId: 'overlay_channel',
155
+ channelName: 'Overlay Service'
156
+ });
157
+ };
158
+ ```
159
+
160
+ ---
161
+
162
+ ## 3. Stop the overlay
163
+
164
+ ```typescript
165
+ const stopOverlay = () => {
166
+ OverlayManager.stopOverlay('OverlayView');
167
+ };
168
+ ```
169
+
170
+ ---
171
+
172
+ ## Example overlay component
173
+
174
+ ```tsx
175
+ import { View, Text } from 'react-native';
176
+
177
+ export default function MyOverlayComponent() {
178
+ return (
179
+ <View>
180
+ <Text>
181
+ Hello from overlay
182
+ </Text>
183
+ </View>
184
+ );
185
+ }
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Overlay resizing
191
+
192
+ Resize the overlay container at runtime:
193
+
194
+ ```typescript
195
+ OverlayManager.resizeOverlay(
196
+ 320,
197
+ 200,
198
+ 'OverlayView'
199
+ );
200
+ ```
201
+
202
+ ---
203
+
204
+ ## Overlay movement
205
+
206
+ Move the overlay window manually from JavaScript:
207
+
208
+ ```typescript
209
+ OverlayManager.startMove('OverlayView');
210
+
211
+ OverlayManager.moveOverlay(
212
+ dx,
213
+ dy,
214
+ 'OverlayView'
215
+ );
216
+
217
+ OverlayManager.commitMove('OverlayView');
218
+ ```
219
+
220
+ ---
221
+
222
+ # API reference
223
+
224
+ ## OverlayManager
225
+
226
+ | Method | Arguments | Returns | Description |
227
+ |-----------------------|---------------------------------------------------------|--------------------|---------------------------------------------|
228
+ | `hasPermission()` | None | `Promise<boolean>` | Checks if overlay permission is granted |
229
+ | `requestPermission()` | None | `void` | Opens Android overlay permission settings |
230
+ | `startOverlay()` | `componentName: string, options?: OverlayOptions` | `void` | Starts the overlay service |
231
+ | `stopOverlay()` | `componentName?: string` | `void` | Removes overlay and stops service if unused |
232
+ | `resizeOverlay()` | `width: number, height: number, componentName?: string` | `void` | Changes overlay window size |
233
+ | `startMove()` | `componentName?: string` | `void` | Prepares overlay movement |
234
+ | `moveOverlay()` | `dx: number, dy: number, componentName?: string` | `void` | Moves overlay by offset |
235
+ | `commitMove()` | `componentName?: string` | `void` | Saves final overlay position |
236
+
237
+ ---
238
+
239
+ ## OverlayOptions
240
+
241
+ ```typescript
242
+ interface OverlayOptions {
243
+ width?: number; // Width of the overlay window in DP
244
+ height?: number; // Height of the overlay window in DP
245
+
246
+ x?: number; // Horizontal coordinates offset in DP. Defaults to 0
247
+ y?: number; // Vertical coordinates offset in DP. Defaults to 150dp
248
+
249
+ gravity?:
250
+ 'top' |
251
+ 'bottom' |
252
+ 'center'; // Gravity anchor for overlay positioning. Defaults to 'bottom'
253
+
254
+ draggable?: boolean; // Enable/disable dragging gestures to move overlay (defaults to true)
255
+ touchable?: boolean; // If false, touch events pass through to windows beneath (defaults to true)
256
+ focusable?: boolean; // If true, overlay can receive keyboard/input focus (defaults to false)
257
+
258
+ foreground?: boolean; // Runs overlay inside a persistent foreground service (defaults to true) so that JS logic continues running when app is minimized
259
+
260
+ notificationTitle?: string; // Custom title for foreground service notification
261
+ notificationText?: string; // Custom message text for foreground service notification
262
+ notificationIcon?: string; // Resdrawable name of the notification icon (e.g. "ic_launcher")
263
+
264
+ channelId?: string; // Notification channel ID (defaults to "OverlayServiceChannel")
265
+ channelName?: string; // Notification channel name (defaults to "Overlay Service")
266
+ }
267
+ ```
@@ -0,0 +1,54 @@
1
+ buildscript {
2
+ repositories {
3
+ google()
4
+ mavenCentral()
5
+ }
6
+ dependencies {
7
+ classpath 'com.android.tools.build:gradle:8.10.0'
8
+ }
9
+ }
10
+
11
+ apply plugin: 'com.android.library'
12
+
13
+ def isNewArchitectureEnabled() {
14
+ return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
15
+ }
16
+
17
+ if (isNewArchitectureEnabled()) {
18
+ apply plugin: "com.facebook.react"
19
+ }
20
+
21
+
22
+ def safeExtGet(prop, fallback) {
23
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
24
+ }
25
+
26
+ android {
27
+ namespace "tech.zmario.androidoverlay"
28
+ compileSdk safeExtGet('compileSdkVersion', 36)
29
+
30
+ defaultConfig {
31
+ minSdk safeExtGet('minSdkVersion', 24)
32
+ targetSdk safeExtGet('targetSdkVersion', 36)
33
+ buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
34
+ }
35
+
36
+ buildTypes {
37
+ release {
38
+ minifyEnabled false
39
+ }
40
+ }
41
+ }
42
+
43
+ repositories {
44
+ google()
45
+ mavenCentral()
46
+ maven {
47
+ url "$rootDir/../node_modules/react-native/android"
48
+ }
49
+ }
50
+
51
+ dependencies {
52
+ implementation("com.facebook.react:react-android")
53
+ implementation("androidx.core:core:1.12.0")
54
+ }
@@ -0,0 +1,9 @@
1
+ pluginManagement {
2
+ includeBuild("../node_modules/@react-native/gradle-plugin")
3
+ }
4
+
5
+ plugins {
6
+ id("com.facebook.react.settings")
7
+ }
8
+
9
+ rootProject.name = "react-native-android-overlay"
@@ -0,0 +1,19 @@
1
+ <manifest xmlns:tools="http://schemas.android.com/tools"
2
+ xmlns:android="http://schemas.android.com/apk/res/android">
3
+ <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
4
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE"
5
+ tools:ignore="ForegroundServicesPolicy" />
6
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
7
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
8
+
9
+ <application>
10
+ <service
11
+ android:name="tech.zmario.androidoverlay.service.OverlayService"
12
+ android:foregroundServiceType="specialUse"
13
+ android:enabled="true"
14
+ android:exported="false">
15
+ <property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
16
+ android:value="Overlay service to display floating view over other applications."/>
17
+ </service>
18
+ </application>
19
+ </manifest>
@@ -0,0 +1,52 @@
1
+ package tech.zmario.androidoverlay;
2
+
3
+ import androidx.annotation.NonNull;
4
+ import com.facebook.react.BaseReactPackage;
5
+ import com.facebook.react.bridge.NativeModule;
6
+ import com.facebook.react.bridge.ReactApplicationContext;
7
+ import com.facebook.react.module.model.ReactModuleInfo;
8
+ import com.facebook.react.module.model.ReactModuleInfoProvider;
9
+ import com.facebook.react.uimanager.ViewManager;
10
+ import java.util.Collections;
11
+ import java.util.HashMap;
12
+ import java.util.List;
13
+ import java.util.Map;
14
+ import tech.zmario.androidoverlay.module.OverlayModule;
15
+
16
+ public class OverlayPackage extends BaseReactPackage {
17
+
18
+ @Override
19
+ public NativeModule getModule(String name, @NonNull ReactApplicationContext reactContext) {
20
+ if (name.equals(NativeAndroidOverlaySpec.NAME)) {
21
+ return new OverlayModule(reactContext);
22
+ }
23
+
24
+ return null;
25
+ }
26
+
27
+ @NonNull
28
+ @Override
29
+ public ReactModuleInfoProvider getReactModuleInfoProvider() {
30
+ return () -> {
31
+ Map<String, ReactModuleInfo> moduleInfos = new HashMap<>();
32
+ boolean isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
33
+
34
+ moduleInfos.put(
35
+ NativeAndroidOverlaySpec.NAME,
36
+ new ReactModuleInfo(
37
+ NativeAndroidOverlaySpec.NAME,
38
+ NativeAndroidOverlaySpec.NAME,
39
+ false,
40
+ false,
41
+ false,
42
+ isTurboModule));
43
+ return moduleInfos;
44
+ };
45
+ }
46
+
47
+ @NonNull
48
+ @Override
49
+ public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
50
+ return Collections.emptyList();
51
+ }
52
+ }
@@ -0,0 +1,133 @@
1
+ package tech.zmario.androidoverlay.module;
2
+
3
+ import android.content.Intent;
4
+ import android.net.Uri;
5
+ import android.os.Build;
6
+ import android.provider.Settings;
7
+ import com.facebook.react.bridge.Promise;
8
+ import com.facebook.react.bridge.ReactApplicationContext;
9
+ import com.facebook.react.bridge.ReactMethod;
10
+ import com.facebook.react.bridge.ReadableMap;
11
+ import tech.zmario.androidoverlay.NativeAndroidOverlaySpec;
12
+ import tech.zmario.androidoverlay.service.OverlayService;
13
+
14
+ public class OverlayModule extends NativeAndroidOverlaySpec {
15
+
16
+ public OverlayModule(ReactApplicationContext reactContext) {
17
+ super(reactContext);
18
+ }
19
+
20
+ @ReactMethod
21
+ public void hasPermission(Promise promise) {
22
+ promise.resolve(Settings.canDrawOverlays(getReactApplicationContext()));
23
+ }
24
+
25
+ @ReactMethod
26
+ public void requestPermission() {
27
+ if (!Settings.canDrawOverlays(getReactApplicationContext())) {
28
+ Intent intent =
29
+ new Intent(
30
+ Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
31
+ Uri.parse("package:" + getReactApplicationContext().getPackageName()));
32
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
33
+ getReactApplicationContext().startActivity(intent);
34
+ }
35
+ }
36
+
37
+ @ReactMethod
38
+ public void startOverlay(String componentName, ReadableMap options) {
39
+ if (!Settings.canDrawOverlays(getReactApplicationContext())) {
40
+ return;
41
+ }
42
+ Intent intent = new Intent(getReactApplicationContext(), OverlayService.class);
43
+ intent.putExtra("componentName", componentName);
44
+
45
+ if (options != null) {
46
+ if (options.hasKey("width")) intent.putExtra("width", options.getDouble("width"));
47
+ if (options.hasKey("height")) intent.putExtra("height", options.getDouble("height"));
48
+ if (options.hasKey("x")) intent.putExtra("x", options.getDouble("x"));
49
+ if (options.hasKey("y")) intent.putExtra("y", options.getDouble("y"));
50
+ if (options.hasKey("gravity")) intent.putExtra("gravity", options.getString("gravity"));
51
+
52
+ if (options.hasKey("draggable"))
53
+ intent.putExtra("draggable", options.getBoolean("draggable"));
54
+
55
+ if (options.hasKey("touchable"))
56
+ intent.putExtra("touchable", options.getBoolean("touchable"));
57
+
58
+ if (options.hasKey("focusable"))
59
+ intent.putExtra("focusable", options.getBoolean("focusable"));
60
+
61
+ if (options.hasKey("notificationTitle"))
62
+ intent.putExtra("notificationTitle", options.getString("notificationTitle"));
63
+
64
+ if (options.hasKey("notificationText"))
65
+ intent.putExtra("notificationText", options.getString("notificationText"));
66
+
67
+ if (options.hasKey("notificationIcon"))
68
+ intent.putExtra("notificationIcon", options.getString("notificationIcon"));
69
+
70
+ if (options.hasKey("channelId")) intent.putExtra("channelId", options.getString("channelId"));
71
+
72
+ if (options.hasKey("channelName"))
73
+ intent.putExtra("channelName", options.getString("channelName"));
74
+
75
+ if (options.hasKey("foreground"))
76
+ intent.putExtra("foreground", options.getBoolean("foreground"));
77
+ }
78
+
79
+ boolean foreground = true;
80
+
81
+ if (options != null && options.hasKey("foreground")) {
82
+ foreground = options.getBoolean("foreground");
83
+ }
84
+
85
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && foreground) {
86
+ getReactApplicationContext().startForegroundService(intent);
87
+ } else {
88
+ getReactApplicationContext().startService(intent);
89
+ }
90
+ }
91
+
92
+ @ReactMethod
93
+ public void stopOverlay(String componentName) {
94
+ if (OverlayService.getInstance() != null) {
95
+ OverlayService.getInstance().stopOverlayInstance(componentName);
96
+ }
97
+ }
98
+
99
+ @ReactMethod
100
+ public void resizeOverlay(double width, double height, String componentName) {
101
+ float density = getReactApplicationContext().getResources().getDisplayMetrics().density;
102
+ int pxWidth = (int) (width * density);
103
+ int pxHeight = (int) (height * density);
104
+
105
+ if (OverlayService.getInstance() != null) {
106
+ OverlayService.getInstance().resizeOverlay(componentName, pxWidth, pxHeight);
107
+ }
108
+ }
109
+
110
+ @ReactMethod
111
+ public void startMove(String componentName) {
112
+ if (OverlayService.getInstance() != null) {
113
+ OverlayService.getInstance().startMove(componentName);
114
+ }
115
+ }
116
+
117
+ @ReactMethod
118
+ public void moveOverlay(double dx, double dy, String componentName) {
119
+ float density = getReactApplicationContext().getResources().getDisplayMetrics().density;
120
+
121
+ if (OverlayService.getInstance() != null) {
122
+ OverlayService.getInstance()
123
+ .moveOverlay(componentName, (int) (dx * density), (int) (dy * density));
124
+ }
125
+ }
126
+
127
+ @ReactMethod
128
+ public void commitMove(String componentName) {
129
+ if (OverlayService.getInstance() != null) {
130
+ OverlayService.getInstance().commitMove(componentName);
131
+ }
132
+ }
133
+ }
@@ -0,0 +1,41 @@
1
+ package tech.zmario.androidoverlay.service;
2
+
3
+ import android.widget.FrameLayout;
4
+ import com.facebook.react.interfaces.fabric.ReactSurface;
5
+
6
+ public class OverlayInstance {
7
+
8
+ private final FrameLayout overlayView;
9
+ private final ReactSurface reactSurface;
10
+ private int initialX = 0;
11
+ private int initialY = 0;
12
+
13
+ public OverlayInstance(FrameLayout overlayView, ReactSurface reactSurface) {
14
+ this.overlayView = overlayView;
15
+ this.reactSurface = reactSurface;
16
+ }
17
+
18
+ public FrameLayout getOverlayView() {
19
+ return overlayView;
20
+ }
21
+
22
+ public ReactSurface getReactSurface() {
23
+ return reactSurface;
24
+ }
25
+
26
+ public int getInitialX() {
27
+ return initialX;
28
+ }
29
+
30
+ public void setInitialX(int initialX) {
31
+ this.initialX = initialX;
32
+ }
33
+
34
+ public int getInitialY() {
35
+ return initialY;
36
+ }
37
+
38
+ public void setInitialY(int initialY) {
39
+ this.initialY = initialY;
40
+ }
41
+ }
@@ -0,0 +1,57 @@
1
+ package tech.zmario.androidoverlay.service;
2
+
3
+ import android.graphics.PixelFormat;
4
+ import android.os.Build;
5
+ import android.view.Gravity;
6
+ import android.view.WindowManager;
7
+
8
+ public class OverlayLayoutParamsBuilder {
9
+
10
+ private OverlayLayoutParamsBuilder() {}
11
+
12
+ public static WindowManager.LayoutParams build(
13
+ double width,
14
+ double height,
15
+ double x,
16
+ double y,
17
+ String gravity,
18
+ boolean focusable,
19
+ boolean touchable,
20
+ float density,
21
+ int screenWidth) {
22
+ int layoutFlag =
23
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
24
+ ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
25
+ : WindowManager.LayoutParams.TYPE_PHONE;
26
+ int widthPx = width > 0 ? (int) (width * density) : (int) (screenWidth * 0.95);
27
+ int heightPx = height > 0 ? (int) (height * density) : (int) (140f * density);
28
+
29
+ int flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
30
+
31
+ if (focusable) {
32
+ flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
33
+ } else {
34
+ flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
35
+ }
36
+ if (!touchable) {
37
+ flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
38
+ }
39
+
40
+ WindowManager.LayoutParams params =
41
+ new WindowManager.LayoutParams(
42
+ widthPx, heightPx, layoutFlag, flags, PixelFormat.TRANSLUCENT);
43
+
44
+ int gravityFlag = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
45
+ if ("top".equalsIgnoreCase(gravity)) {
46
+ gravityFlag = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
47
+ } else if ("center".equalsIgnoreCase(gravity)) {
48
+ gravityFlag = Gravity.CENTER;
49
+ }
50
+
51
+ params.gravity = gravityFlag;
52
+ params.x = (int) (x * density);
53
+ params.y = (int) (y * density);
54
+
55
+ return params;
56
+ }
57
+ }