anchordb-lens-link 1.3.0 → 1.3.2
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/README.md +48 -1
- package/android/build.gradle +5 -3
- package/android/src/main/AndroidManifest.xml +28 -0
- package/android/src/main/java/dev/anchordb/lenslink/AnchorLensLinkModule.kt +187 -5
- package/android/src/main/java/dev/anchordb/lenslink/LensLinkKeepAliveService.kt +89 -0
- package/android/src/main/res/drawable/anchor_lens_link_notification.xml +10 -0
- package/dist/cjs/app.d.ts +17 -25
- package/dist/cjs/app.d.ts.map +1 -1
- package/dist/cjs/app.js +45 -14
- package/dist/cjs/app.js.map +1 -1
- package/dist/cjs/index.d.ts +8 -3
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +14 -3
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/keep-alive.d.ts +40 -0
- package/dist/cjs/keep-alive.d.ts.map +1 -0
- package/dist/cjs/keep-alive.js +53 -0
- package/dist/cjs/keep-alive.js.map +1 -0
- package/dist/cjs/native.d.ts +57 -0
- package/dist/cjs/native.d.ts.map +1 -0
- package/dist/cjs/native.js +23 -0
- package/dist/cjs/native.js.map +1 -0
- package/dist/esm/app.d.ts +17 -25
- package/dist/esm/app.d.ts.map +1 -1
- package/dist/esm/app.js +44 -13
- package/dist/esm/app.js.map +1 -1
- package/dist/esm/index.d.ts +8 -3
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +5 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/keep-alive.d.ts +40 -0
- package/dist/esm/keep-alive.d.ts.map +1 -0
- package/dist/esm/keep-alive.js +43 -0
- package/dist/esm/keep-alive.js.map +1 -0
- package/dist/esm/native.d.ts +57 -0
- package/dist/esm/native.d.ts.map +1 -0
- package/dist/esm/native.js +19 -0
- package/dist/esm/native.js.map +1 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
**Open a QA build's AnchorDB database in Anchor Lens, on the same Android phone, from a file.** No
|
|
4
4
|
laptop, no relay, no pairing code.
|
|
5
5
|
|
|
6
|
+
**[Overview](https://www.nisalanadeera.com/projects/anchordb)** · **[Report a bug or get support](https://www.nisalanadeera.com/projects/anchordb#feedback)**
|
|
7
|
+
|
|
6
8
|
```bash
|
|
7
9
|
npm install anchordb-lens-link
|
|
8
10
|
```
|
|
@@ -41,6 +43,42 @@ Lens opens the app's database live. Edits in either app show up in the other.
|
|
|
41
43
|
The file is rewritten on every launch, because the port and the secret change. It works only while the
|
|
42
44
|
app is running; if Lens says the app closed, open the app and pick the file again.
|
|
43
45
|
|
|
46
|
+
## Staying alive in the background
|
|
47
|
+
|
|
48
|
+
Android stops apps in the background to save power, and some phones — Xiaomi's especially — do it
|
|
49
|
+
within seconds of switching away. So while the link is on, the app runs an Android **foreground
|
|
50
|
+
service** with an ongoing notification (*Anchor Lens Link on*, then *Anchor Lens connected*), which is
|
|
51
|
+
what Android lets an app use to keep running. Anchor Lens does the same while it is connected, so you
|
|
52
|
+
can switch between the two.
|
|
53
|
+
|
|
54
|
+
It starts with the link, so call `enableLensLink` while the app is on screen: Android 12 and later
|
|
55
|
+
refuse to start one from the background. `keepAlive: false` turns it off, and `link.keepAliveError`
|
|
56
|
+
says why it could not start.
|
|
57
|
+
|
|
58
|
+
Two settings make it dependable. Put them on a QA screen:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import {
|
|
62
|
+
askBatteryUnrestricted,
|
|
63
|
+
askNotificationPermission,
|
|
64
|
+
backgroundStatus,
|
|
65
|
+
openBackgroundSettings,
|
|
66
|
+
} from "anchordb-lens-link";
|
|
67
|
+
|
|
68
|
+
const status = backgroundStatus(); // { notifications, batteryUnrestricted, keepingAlive, manufacturer, vendorSettings }
|
|
69
|
+
await askNotificationPermission(); // Android 13+: lets the notification show
|
|
70
|
+
askBatteryUnrestricted(); // Android's "let this app always run in the background?" dialog
|
|
71
|
+
if (status?.vendorSettings) openBackgroundSettings(); // Xiaomi: Autostart, and Battery saver
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
On a Xiaomi phone also turn on **Autostart**, set **Battery saver** to **No restrictions**, and lock the
|
|
75
|
+
app in Recents.
|
|
76
|
+
|
|
77
|
+
The package adds `FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_SPECIAL_USE`, `POST_NOTIFICATIONS` and
|
|
78
|
+
`REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` to the app's manifest, with a `specialUse` foreground service.
|
|
79
|
+
That suits a QA build; a Play Store build that shipped the package would have to justify them — one
|
|
80
|
+
more reason to keep it out of production.
|
|
81
|
+
|
|
44
82
|
## Keep it out of production
|
|
45
83
|
|
|
46
84
|
A link exposes the database to Anchor Lens on the phone, so:
|
|
@@ -69,9 +107,10 @@ enableLensLink(db, options?): Promise<LensLink>
|
|
|
69
107
|
| `allowInProduction` | Required in a release build |
|
|
70
108
|
| `readOnly` | Refuse every write from Lens |
|
|
71
109
|
| `anchorVersion`, `platform` | Reported to Lens |
|
|
110
|
+
| `keepAlive` | Keep the app running in the background while the link is on. Default `true` |
|
|
72
111
|
| `onConnectionsChange(count)` | Lens connections opening and closing |
|
|
73
112
|
|
|
74
|
-
`LensLink` has `port`, `fileName`, `filePath`, `connections`, `agent` and `close()`. Calling
|
|
113
|
+
`LensLink` has `port`, `fileName`, `filePath`, `connections`, `keepAliveError`, `agent` and `close()`. Calling
|
|
75
114
|
`enableLensLink` again for the same database replaces the running link, so it is safe in an effect and
|
|
76
115
|
through Fast Refresh.
|
|
77
116
|
|
|
@@ -85,3 +124,11 @@ For a Lens client: `readLensLink(fileText)` returns `{ app, database, port, secr
|
|
|
85
124
|
React Native app needs `expo-modules-core` installed). iOS is not supported yet.
|
|
86
125
|
- `anchordb` 1.3 or later, whose inspector accepts a shared secret.
|
|
87
126
|
- Anchor Lens with **Open app from file**.
|
|
127
|
+
|
|
128
|
+
## Bugs, support and feedback
|
|
129
|
+
|
|
130
|
+
Report a bug, ask for help or suggest a feature on the [AnchorDB project page](https://www.nisalanadeera.com/projects/anchordb#feedback) —
|
|
131
|
+
choose `anchordb-lens-link` as the package, and the reply comes by email.
|
|
132
|
+
|
|
133
|
+
Include the version (`npm ls anchordb`), where it runs, the smallest snippet that reproduces it, and
|
|
134
|
+
the full error.
|
package/android/build.gradle
CHANGED
|
@@ -4,13 +4,13 @@ plugins {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
group = 'dev.anchordb'
|
|
7
|
-
version = '1.3.
|
|
7
|
+
version = '1.3.2'
|
|
8
8
|
|
|
9
9
|
android {
|
|
10
10
|
namespace "dev.anchordb.lenslink"
|
|
11
11
|
defaultConfig {
|
|
12
|
-
versionCode
|
|
13
|
-
versionName '1.3.
|
|
12
|
+
versionCode 3
|
|
13
|
+
versionName '1.3.2'
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -18,4 +18,6 @@ dependencies {
|
|
|
18
18
|
// The WebSocket server on 127.0.0.1. Anchor Lens connects with React Native's built-in WebSocket, so
|
|
19
19
|
// this is the only native networking code on either side.
|
|
20
20
|
implementation "org.java-websocket:Java-WebSocket:1.6.0"
|
|
21
|
+
// NotificationCompat and ServiceCompat, for the foreground service that keeps the app running.
|
|
22
|
+
implementation "androidx.core:core-ktx:1.17.0"
|
|
21
23
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
|
+
|
|
3
|
+
<!-- Keep the app running while Anchor Lens Link is on: a foreground service with a notification. -->
|
|
4
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
5
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
|
6
|
+
<!-- Android 13 and later: lets that notification show. -->
|
|
7
|
+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
8
|
+
<!-- The system "let this app always run in the background?" dialog. -->
|
|
9
|
+
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
|
10
|
+
|
|
11
|
+
<!-- Phone makers' own background-running screens, so the app can check they exist before opening one. -->
|
|
12
|
+
<queries>
|
|
13
|
+
<package android:name="com.miui.securitycenter" />
|
|
14
|
+
<package android:name="com.coloros.safecenter" />
|
|
15
|
+
<package android:name="com.vivo.permissionmanager" />
|
|
16
|
+
</queries>
|
|
17
|
+
|
|
18
|
+
<application>
|
|
19
|
+
<service
|
|
20
|
+
android:name="dev.anchordb.lenslink.LensLinkKeepAliveService"
|
|
21
|
+
android:exported="false"
|
|
22
|
+
android:foregroundServiceType="specialUse">
|
|
23
|
+
<property
|
|
24
|
+
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
|
25
|
+
android:value="Keeps a development database inspector connection (Anchor Lens Link) open while the app is in the background." />
|
|
26
|
+
</service>
|
|
27
|
+
</application>
|
|
28
|
+
</manifest>
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
package dev.anchordb.lenslink
|
|
2
2
|
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.annotation.SuppressLint
|
|
5
|
+
import android.app.NotificationManager
|
|
6
|
+
import android.content.ComponentName
|
|
7
|
+
import android.content.Context
|
|
8
|
+
import android.content.Intent
|
|
9
|
+
import android.content.pm.PackageManager
|
|
10
|
+
import android.net.Uri
|
|
11
|
+
import android.os.Build
|
|
3
12
|
import android.os.Bundle
|
|
13
|
+
import android.os.PowerManager
|
|
14
|
+
import android.provider.Settings
|
|
15
|
+
import androidx.core.content.ContextCompat
|
|
16
|
+
import expo.modules.interfaces.permissions.Permissions
|
|
4
17
|
import expo.modules.kotlin.Promise
|
|
5
18
|
import expo.modules.kotlin.exception.CodedException
|
|
6
19
|
import expo.modules.kotlin.exception.Exceptions
|
|
@@ -20,11 +33,12 @@ import java.util.concurrent.atomic.AtomicBoolean
|
|
|
20
33
|
import java.util.concurrent.atomic.AtomicInteger
|
|
21
34
|
|
|
22
35
|
/**
|
|
23
|
-
* The native half of Anchor Lens Link: a WebSocket server on this phone's loopback address,
|
|
24
|
-
*
|
|
36
|
+
* The native half of Anchor Lens Link: a WebSocket server on this phone's loopback address, the link file
|
|
37
|
+
* in Android/media/<package>/anchor-lens/, and what keeps the app running in the background.
|
|
25
38
|
*
|
|
26
|
-
* Only transport and
|
|
27
|
-
* JavaScript, in anchordb itself — this module never sees a secret it checks, or a
|
|
39
|
+
* Only transport, files and Android settings live here. Authentication, the inspector protocol and the
|
|
40
|
+
* database are all JavaScript, in anchordb itself — this module never sees a secret it checks, or a
|
|
41
|
+
* document.
|
|
28
42
|
*/
|
|
29
43
|
class AnchorLensLinkModule : Module() {
|
|
30
44
|
private var server: LinkServer? = null
|
|
@@ -68,8 +82,41 @@ class AnchorLensLinkModule : Module() {
|
|
|
68
82
|
linkFile(name).delete()
|
|
69
83
|
}
|
|
70
84
|
|
|
85
|
+
AsyncFunction("startKeepAlive") { title: String, text: String ->
|
|
86
|
+
startKeepAliveNow(title, text)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
Function("updateKeepAlive") { title: String, text: String ->
|
|
90
|
+
updateKeepAliveNow(title, text)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
Function("stopKeepAlive") {
|
|
94
|
+
stopKeepAliveNow()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
Function("backgroundStatus") {
|
|
98
|
+
backgroundStatusNow()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
AsyncFunction("askNotificationPermission") { promise: Promise ->
|
|
102
|
+
askNotificationPermissionNow(promise)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
Function("askBatteryUnrestricted") {
|
|
106
|
+
askBatteryUnrestrictedNow()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
Function("openBackgroundSettings") {
|
|
110
|
+
openBackgroundSettingsNow()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
Function("openNotificationSettings") {
|
|
114
|
+
openNotificationSettingsNow()
|
|
115
|
+
}
|
|
116
|
+
|
|
71
117
|
OnDestroy {
|
|
72
118
|
stopServerNow()
|
|
119
|
+
stopKeepAliveNow()
|
|
73
120
|
}
|
|
74
121
|
}
|
|
75
122
|
|
|
@@ -86,7 +133,7 @@ class AnchorLensLinkModule : Module() {
|
|
|
86
133
|
server?.closeConnection(id)
|
|
87
134
|
}
|
|
88
135
|
|
|
89
|
-
private fun context() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
|
|
136
|
+
private fun context(): Context = appContext.reactContext ?: throw Exceptions.ReactContextLost()
|
|
90
137
|
|
|
91
138
|
@Suppress("DEPRECATION")
|
|
92
139
|
private fun linkFile(name: String): File {
|
|
@@ -97,8 +144,137 @@ class AnchorLensLinkModule : Module() {
|
|
|
97
144
|
return File(File(media, "anchor-lens"), name)
|
|
98
145
|
}
|
|
99
146
|
|
|
147
|
+
// region Keeping the app running
|
|
148
|
+
|
|
149
|
+
private fun startKeepAliveNow(title: String, text: String) {
|
|
150
|
+
val context = context()
|
|
151
|
+
val intent =
|
|
152
|
+
Intent(context, LensLinkKeepAliveService::class.java)
|
|
153
|
+
.putExtra(LensLinkKeepAliveService.EXTRA_TITLE, title)
|
|
154
|
+
.putExtra(LensLinkKeepAliveService.EXTRA_TEXT, text)
|
|
155
|
+
try {
|
|
156
|
+
ContextCompat.startForegroundService(context, intent)
|
|
157
|
+
} catch (err: IllegalStateException) {
|
|
158
|
+
// Android 12 and later refuse to start one while the app is in the background.
|
|
159
|
+
throw KeepAliveNotAllowed(err)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private fun updateKeepAliveNow(title: String, text: String) {
|
|
164
|
+
if (!LensLinkKeepAliveService.running) return
|
|
165
|
+
val context = context()
|
|
166
|
+
try {
|
|
167
|
+
context
|
|
168
|
+
.getSystemService(NotificationManager::class.java)
|
|
169
|
+
?.notify(LensLinkKeepAliveService.NOTIFICATION_ID, LensLinkKeepAliveService.notification(context, title, text))
|
|
170
|
+
} catch (_: SecurityException) {
|
|
171
|
+
// Without notification permission the service keeps running; only its text is not shown.
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private fun stopKeepAliveNow() {
|
|
176
|
+
val context = appContext.reactContext ?: return
|
|
177
|
+
context.stopService(Intent(context, LensLinkKeepAliveService::class.java))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private fun backgroundStatusNow(): Map<String, Any> {
|
|
181
|
+
val context = context()
|
|
182
|
+
val notifications =
|
|
183
|
+
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
|
|
184
|
+
ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) ==
|
|
185
|
+
PackageManager.PERMISSION_GRANTED
|
|
186
|
+
val power = context.getSystemService(PowerManager::class.java)
|
|
187
|
+
return mapOf(
|
|
188
|
+
"notifications" to (if (notifications) "granted" else "denied"),
|
|
189
|
+
"batteryUnrestricted" to (power?.isIgnoringBatteryOptimizations(context.packageName) ?: true),
|
|
190
|
+
"keepingAlive" to LensLinkKeepAliveService.running,
|
|
191
|
+
"manufacturer" to Build.MANUFACTURER,
|
|
192
|
+
"vendorSettings" to (vendorScreen(context) != null),
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private fun askNotificationPermissionNow(promise: Promise) {
|
|
197
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
198
|
+
// Before Android 13 notifications need no permission.
|
|
199
|
+
promise.resolve(
|
|
200
|
+
Bundle().apply {
|
|
201
|
+
putBoolean("granted", true)
|
|
202
|
+
putBoolean("canAskAgain", true)
|
|
203
|
+
},
|
|
204
|
+
)
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
Permissions.askForPermissionsWithPermissionsManager(
|
|
208
|
+
appContext.permissions,
|
|
209
|
+
promise,
|
|
210
|
+
Manifest.permission.POST_NOTIFICATIONS,
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
@SuppressLint("BatteryLife")
|
|
215
|
+
private fun askBatteryUnrestrictedNow(): Boolean {
|
|
216
|
+
val context = context()
|
|
217
|
+
val power = context.getSystemService(PowerManager::class.java)
|
|
218
|
+
if (power?.isIgnoringBatteryOptimizations(context.packageName) == true) return true
|
|
219
|
+
return openScreen(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, Uri.parse("package:${context.packageName}"))) ||
|
|
220
|
+
openScreen(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private fun openBackgroundSettingsNow(): String {
|
|
224
|
+
val context = context()
|
|
225
|
+
val vendor = vendorScreen(context)
|
|
226
|
+
return when {
|
|
227
|
+
vendor != null && openScreen(vendor) -> "vendor"
|
|
228
|
+
openScreen(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:${context.packageName}"))) -> "app"
|
|
229
|
+
else -> "none"
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private fun openNotificationSettingsNow(): Boolean {
|
|
234
|
+
val context = context()
|
|
235
|
+
val intent =
|
|
236
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
237
|
+
Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
|
|
238
|
+
} else {
|
|
239
|
+
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:${context.packageName}"))
|
|
240
|
+
}
|
|
241
|
+
return openScreen(intent)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private fun openScreen(intent: Intent): Boolean {
|
|
245
|
+
val activity = appContext.currentActivity
|
|
246
|
+
return try {
|
|
247
|
+
if (activity != null) {
|
|
248
|
+
activity.startActivity(intent)
|
|
249
|
+
} else {
|
|
250
|
+
context().startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
|
251
|
+
}
|
|
252
|
+
true
|
|
253
|
+
} catch (_: Exception) {
|
|
254
|
+
false
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private fun vendorScreen(context: Context): Intent? {
|
|
259
|
+
val screens = VENDOR_SCREENS[Build.MANUFACTURER.lowercase()] ?: return null
|
|
260
|
+
return screens
|
|
261
|
+
.map { (pkg, activity) -> Intent().setComponent(ComponentName(pkg, activity)) }
|
|
262
|
+
.firstOrNull { it.resolveActivity(context.packageManager) != null }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// endregion
|
|
266
|
+
|
|
100
267
|
companion object {
|
|
101
268
|
private val FILE_NAME = Regex("^[A-Za-z0-9._-]{1,120}\\.anchorlens$")
|
|
269
|
+
|
|
270
|
+
// Makers whose phones stop background apps beyond what Android does, with the screen that lets an app
|
|
271
|
+
// keep running. Redmi and POCO phones report "Xiaomi".
|
|
272
|
+
private val VENDOR_SCREENS =
|
|
273
|
+
mapOf(
|
|
274
|
+
"xiaomi" to listOf("com.miui.securitycenter" to "com.miui.permcenter.autostart.AutoStartManagementActivity"),
|
|
275
|
+
"oppo" to listOf("com.coloros.safecenter" to "com.coloros.safecenter.permission.startup.StartupAppListActivity"),
|
|
276
|
+
"vivo" to listOf("com.vivo.permissionmanager" to "com.vivo.permissionmanager.activity.BgStartUpManagerActivity"),
|
|
277
|
+
)
|
|
102
278
|
}
|
|
103
279
|
}
|
|
104
280
|
|
|
@@ -108,6 +284,12 @@ private class InvalidLinkFileName(name: String) :
|
|
|
108
284
|
private class NoMediaDirectory :
|
|
109
285
|
CodedException("This device has no shared media storage to write the Anchor Lens link file to.")
|
|
110
286
|
|
|
287
|
+
private class KeepAliveNotAllowed(cause: Throwable) :
|
|
288
|
+
CodedException(
|
|
289
|
+
"Android did not let the app keep running in the background, because it was not on screen. Turn the link on while the app is open.",
|
|
290
|
+
cause,
|
|
291
|
+
)
|
|
292
|
+
|
|
111
293
|
/** One launch's server. A stopped WebSocketServer cannot be started again, so each start makes a new one. */
|
|
112
294
|
private class LinkServer(
|
|
113
295
|
private val module: AnchorLensLinkModule,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
package dev.anchordb.lenslink
|
|
2
|
+
|
|
3
|
+
import android.app.Notification
|
|
4
|
+
import android.app.NotificationChannel
|
|
5
|
+
import android.app.NotificationManager
|
|
6
|
+
import android.app.PendingIntent
|
|
7
|
+
import android.app.Service
|
|
8
|
+
import android.content.Context
|
|
9
|
+
import android.content.Intent
|
|
10
|
+
import android.content.pm.ServiceInfo
|
|
11
|
+
import android.os.Build
|
|
12
|
+
import android.os.IBinder
|
|
13
|
+
import androidx.core.app.NotificationCompat
|
|
14
|
+
import androidx.core.app.ServiceCompat
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Keeps the app's process running while Anchor Lens Link is on — or, in Anchor Lens, while it is connected
|
|
18
|
+
* to an app.
|
|
19
|
+
*
|
|
20
|
+
* Android freezes and then stops apps in the background, and some phones (Xiaomi's especially) do it
|
|
21
|
+
* within seconds of switching away. A foreground service, with its ongoing notification, is what Android
|
|
22
|
+
* lets an app use to keep running, so the link survives switching between the two apps. Found on a phone.
|
|
23
|
+
*/
|
|
24
|
+
class LensLinkKeepAliveService : Service() {
|
|
25
|
+
override fun onBind(intent: Intent?): IBinder? = null
|
|
26
|
+
|
|
27
|
+
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
28
|
+
val title = intent?.getStringExtra(EXTRA_TITLE) ?: "Anchor Lens Link"
|
|
29
|
+
val text = intent?.getStringExtra(EXTRA_TEXT) ?: ""
|
|
30
|
+
val type =
|
|
31
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
32
|
+
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
|
33
|
+
} else {
|
|
34
|
+
0
|
|
35
|
+
}
|
|
36
|
+
ServiceCompat.startForeground(this, NOTIFICATION_ID, notification(this, title, text), type)
|
|
37
|
+
running = true
|
|
38
|
+
// Not restarted after being stopped: the JavaScript that answers Lens would not be running anyway.
|
|
39
|
+
return START_NOT_STICKY
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
override fun onDestroy() {
|
|
43
|
+
running = false
|
|
44
|
+
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
|
45
|
+
super.onDestroy()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
companion object {
|
|
49
|
+
const val NOTIFICATION_ID = 0x4c4c
|
|
50
|
+
const val CHANNEL_ID = "anchor-lens-link"
|
|
51
|
+
const val EXTRA_TITLE = "title"
|
|
52
|
+
const val EXTRA_TEXT = "text"
|
|
53
|
+
|
|
54
|
+
@Volatile
|
|
55
|
+
var running = false
|
|
56
|
+
|
|
57
|
+
fun notification(context: Context, title: String, text: String): Notification {
|
|
58
|
+
ensureChannel(context)
|
|
59
|
+
val launch = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
60
|
+
val open =
|
|
61
|
+
launch?.let {
|
|
62
|
+
PendingIntent.getActivity(context, 0, it, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
|
|
63
|
+
}
|
|
64
|
+
val builder =
|
|
65
|
+
NotificationCompat.Builder(context, CHANNEL_ID)
|
|
66
|
+
.setSmallIcon(R.drawable.anchor_lens_link_notification)
|
|
67
|
+
.setContentTitle(title)
|
|
68
|
+
.setContentText(text)
|
|
69
|
+
.setStyle(NotificationCompat.BigTextStyle().bigText(text))
|
|
70
|
+
.setOngoing(true)
|
|
71
|
+
.setOnlyAlertOnce(true)
|
|
72
|
+
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
73
|
+
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
|
74
|
+
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
|
|
75
|
+
if (open != null) builder.setContentIntent(open)
|
|
76
|
+
return builder.build()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private fun ensureChannel(context: Context) {
|
|
80
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
|
81
|
+
val manager = context.getSystemService(NotificationManager::class.java) ?: return
|
|
82
|
+
if (manager.getNotificationChannel(CHANNEL_ID) != null) return
|
|
83
|
+
val channel = NotificationChannel(CHANNEL_ID, "Anchor Lens Link", NotificationManager.IMPORTANCE_LOW)
|
|
84
|
+
channel.description = "Shown while this app keeps running for Anchor Lens."
|
|
85
|
+
channel.setShowBadge(false)
|
|
86
|
+
manager.createNotificationChannel(channel)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2
|
+
android:width="24dp"
|
|
3
|
+
android:height="24dp"
|
|
4
|
+
android:viewportWidth="24"
|
|
5
|
+
android:viewportHeight="24">
|
|
6
|
+
<!-- An eye: something is looking at this app's data. -->
|
|
7
|
+
<path
|
|
8
|
+
android:fillColor="#FFFFFFFF"
|
|
9
|
+
android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z" />
|
|
10
|
+
</vector>
|
package/dist/cjs/app.d.ts
CHANGED
|
@@ -1,31 +1,14 @@
|
|
|
1
1
|
import { InspectorAgent, type AnchorDB } from "anchordb";
|
|
2
|
+
import { type LensLinkNative } from "./native.js";
|
|
2
3
|
/**
|
|
3
4
|
* The app half of Anchor Lens Link.
|
|
4
5
|
*
|
|
5
6
|
* `enableLensLink(db)` starts a WebSocket server on 127.0.0.1, puts an `InspectorAgent` on the database
|
|
6
|
-
* behind it,
|
|
7
|
-
*
|
|
8
|
-
* secret made for this launch, which exists
|
|
7
|
+
* behind it, writes the link file Lens opens, and keeps the app running in the background while the link
|
|
8
|
+
* is on. Every connection is an ordinary inspector session — the same protocol, capability checks and
|
|
9
|
+
* Model API writes as through the relay — authenticated with a secret made for this launch, which exists
|
|
10
|
+
* only inside the sealed file.
|
|
9
11
|
*/
|
|
10
|
-
/** An event from the native module. */
|
|
11
|
-
export interface LensLinkNativeEvent {
|
|
12
|
-
id: number;
|
|
13
|
-
data?: string;
|
|
14
|
-
code?: number;
|
|
15
|
-
}
|
|
16
|
-
/** The native module's surface. Exported so a test, or another runtime, can supply its own. */
|
|
17
|
-
export interface LensLinkNative {
|
|
18
|
-
startServer(): Promise<number>;
|
|
19
|
-
stopServer(): Promise<void>;
|
|
20
|
-
send(id: number, data: string): boolean;
|
|
21
|
-
closeConnection(id: number): void;
|
|
22
|
-
packageName(): string;
|
|
23
|
-
writeLinkFile(name: string, contents: string): string;
|
|
24
|
-
deleteLinkFile(name: string): boolean;
|
|
25
|
-
addListener(event: "onOpen" | "onMessage" | "onClose", listener: (event: LensLinkNativeEvent) => void): {
|
|
26
|
-
remove(): void;
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
12
|
export interface LensLinkOptions {
|
|
30
13
|
/**
|
|
31
14
|
* Required in a release build — which includes a QA APK. Lens Link exposes the database to Anchor
|
|
@@ -37,6 +20,12 @@ export interface LensLinkOptions {
|
|
|
37
20
|
/** Reported to Lens. */
|
|
38
21
|
anchorVersion?: string;
|
|
39
22
|
platform?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Keep the app running in the background while the link is on, with an ongoing notification — an
|
|
25
|
+
* Android foreground service. On by default: without it Android stops the app soon after you switch to
|
|
26
|
+
* Lens, and some phones within seconds. `backgroundStatus()` reports the settings that help it hold.
|
|
27
|
+
*/
|
|
28
|
+
keepAlive?: boolean;
|
|
40
29
|
/** Called whenever a Lens connection opens or closes, with how many are open. */
|
|
41
30
|
onConnectionsChange?: (count: number) => void;
|
|
42
31
|
/** The native module. Defaults to the one this package installs. */
|
|
@@ -52,14 +41,17 @@ export interface LensLink {
|
|
|
52
41
|
readonly filePath: string;
|
|
53
42
|
/** Lens connections open right now. */
|
|
54
43
|
readonly connections: number;
|
|
55
|
-
/**
|
|
44
|
+
/** Why the app could not be kept running in the background; null when it is, or when keepAlive is off. */
|
|
45
|
+
readonly keepAliveError: string | null;
|
|
46
|
+
/** Stop listening, close every Lens connection, delete the link file, and stop keeping the app running. */
|
|
56
47
|
close(): Promise<void>;
|
|
57
48
|
}
|
|
58
49
|
/**
|
|
59
50
|
* Let Anchor Lens on this phone open `db`. For development and QA builds.
|
|
60
51
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
52
|
+
* Call it while the app is on screen — at startup, say — since Android only lets a foreground app start
|
|
53
|
+
* keeping itself running. Calling it again for the same database replaces the running link, so it is safe
|
|
54
|
+
* in an effect or after a reload. Resolves once the link file is written.
|
|
63
55
|
*/
|
|
64
56
|
export declare function enableLensLink(db: AnchorDB, options?: LensLinkOptions): Promise<LensLink>;
|
|
65
57
|
//# sourceMappingURL=app.d.ts.map
|
package/dist/cjs/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EAGd,KAAK,QAAQ,EAEd,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EAGd,KAAK,QAAQ,EAEd,MAAM,UAAU,CAAC;AAElB,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAEhE;;;;;;;;GAQG;AAEH,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,oCAAoC;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,wBAAwB;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,iFAAiF;IACjF,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,oEAAoE;IACpE,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,6BAA6B;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,gHAAgH;IAChH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,uCAAuC;IACvC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,0GAA0G;IAC1G,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,2GAA2G;IAC3G,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAOD;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAOnG"}
|
package/dist/cjs/app.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.enableLensLink = enableLensLink;
|
|
4
4
|
const anchordb_1 = require("anchordb");
|
|
5
5
|
const link_file_ts_1 = require("./link-file.js");
|
|
6
|
+
const native_ts_1 = require("./native.js");
|
|
6
7
|
// On globalThis, not in module scope: Fast Refresh re-runs this module, and a registry that reset with it
|
|
7
8
|
// would leave the previous agent answering on the previous server's events.
|
|
8
9
|
const registry = (globalThis.__anchordbLensLinks ??=
|
|
@@ -10,12 +11,13 @@ const registry = (globalThis.__anchordbLensLinks ??=
|
|
|
10
11
|
/**
|
|
11
12
|
* Let Anchor Lens on this phone open `db`. For development and QA builds.
|
|
12
13
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
14
|
+
* Call it while the app is on screen — at startup, say — since Android only lets a foreground app start
|
|
15
|
+
* keeping itself running. Calling it again for the same database replaces the running link, so it is safe
|
|
16
|
+
* in an effect or after a reload. Resolves once the link file is written.
|
|
15
17
|
*/
|
|
16
18
|
async function enableLensLink(db, options = {}) {
|
|
17
19
|
(0, anchordb_1.assertInspectorAllowed)(db.name, options.allowInProduction);
|
|
18
|
-
const native = options.native ?? nativeModule();
|
|
20
|
+
const native = options.native ?? (0, native_ts_1.nativeModule)();
|
|
19
21
|
await registry.get(db.name)?.close();
|
|
20
22
|
const link = await start(db, native, options);
|
|
21
23
|
registry.set(db.name, link);
|
|
@@ -26,6 +28,7 @@ async function start(db, native, options) {
|
|
|
26
28
|
// proves the same value.
|
|
27
29
|
const secret = (0, link_file_ts_1.randomHex)(32).toUpperCase();
|
|
28
30
|
const platform = options.platform ?? "android";
|
|
31
|
+
const fileName = `${db.name.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 100)}.anchorlens`;
|
|
29
32
|
const agent = new anchordb_1.InspectorAgent(db, {
|
|
30
33
|
auth: { sharedSecret: secret },
|
|
31
34
|
platform,
|
|
@@ -33,7 +36,23 @@ async function start(db, native, options) {
|
|
|
33
36
|
...(options.readOnly ? { readOnly: true } : {}),
|
|
34
37
|
});
|
|
35
38
|
const sockets = new Map();
|
|
36
|
-
|
|
39
|
+
let keepingAlive = false;
|
|
40
|
+
let keepAliveError = null;
|
|
41
|
+
/** The notification: whether Lens is connected right now. */
|
|
42
|
+
const notice = () => sockets.size > 0
|
|
43
|
+
? ["Anchor Lens connected", `Anchor Lens is reading ${db.name}. This app keeps running while you use Lens.`]
|
|
44
|
+
: ["Anchor Lens Link on", `Anchor Lens can open ${db.name} from ${fileName}.`];
|
|
45
|
+
const report = () => {
|
|
46
|
+
if (keepingAlive) {
|
|
47
|
+
try {
|
|
48
|
+
native.updateKeepAlive(...notice());
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// The notification's wording is a courtesy; the connection does not depend on it.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
options.onConnectionsChange?.(sockets.size);
|
|
55
|
+
};
|
|
37
56
|
const subscriptions = [
|
|
38
57
|
native.addListener("onOpen", ({ id }) => {
|
|
39
58
|
const socket = new NativeSocket(native, id);
|
|
@@ -54,7 +73,6 @@ async function start(db, native, options) {
|
|
|
54
73
|
report();
|
|
55
74
|
}),
|
|
56
75
|
];
|
|
57
|
-
const fileName = `${db.name.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 100)}.anchorlens`;
|
|
58
76
|
let port;
|
|
59
77
|
let filePath;
|
|
60
78
|
try {
|
|
@@ -78,6 +96,16 @@ async function start(db, native, options) {
|
|
|
78
96
|
await agent.close();
|
|
79
97
|
throw err;
|
|
80
98
|
}
|
|
99
|
+
if (options.keepAlive !== false) {
|
|
100
|
+
try {
|
|
101
|
+
await native.startKeepAlive(...notice());
|
|
102
|
+
keepingAlive = true;
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
// The link still works while the app is on screen, so this is reported, not thrown.
|
|
106
|
+
keepAliveError = err.message;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
81
109
|
let closed = false;
|
|
82
110
|
const link = {
|
|
83
111
|
agent,
|
|
@@ -87,6 +115,9 @@ async function start(db, native, options) {
|
|
|
87
115
|
get connections() {
|
|
88
116
|
return sockets.size;
|
|
89
117
|
},
|
|
118
|
+
get keepAliveError() {
|
|
119
|
+
return keepAliveError;
|
|
120
|
+
},
|
|
90
121
|
close: async () => {
|
|
91
122
|
if (closed)
|
|
92
123
|
return;
|
|
@@ -100,6 +131,15 @@ async function start(db, native, options) {
|
|
|
100
131
|
socket.closed();
|
|
101
132
|
}
|
|
102
133
|
sockets.clear();
|
|
134
|
+
if (keepingAlive) {
|
|
135
|
+
keepingAlive = false;
|
|
136
|
+
try {
|
|
137
|
+
native.stopKeepAlive();
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Already stopped, or the app is shutting down.
|
|
141
|
+
}
|
|
142
|
+
}
|
|
103
143
|
try {
|
|
104
144
|
native.deleteLinkFile(fileName);
|
|
105
145
|
}
|
|
@@ -154,13 +194,4 @@ class NativeSocket {
|
|
|
154
194
|
listener(event);
|
|
155
195
|
}
|
|
156
196
|
}
|
|
157
|
-
function nativeModule() {
|
|
158
|
-
const modules = globalThis.expo?.modules;
|
|
159
|
-
const found = modules?.AnchorLensLink;
|
|
160
|
-
if (!found) {
|
|
161
|
-
throw new Error("Anchor Lens Link needs its native module, and this build does not have it. It runs on Android, in a " +
|
|
162
|
-
"development or release build (not Expo Go): install anchordb-lens-link, then rebuild the app.");
|
|
163
|
-
}
|
|
164
|
-
return found;
|
|
165
|
-
}
|
|
166
197
|
//# sourceMappingURL=app.js.map
|