@tamer4lynx/tamer-dev-client 0.0.1

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.
@@ -0,0 +1,28 @@
1
+ package com.nanofuxion.tamerdevclient.nsd
2
+
3
+ import android.net.nsd.NsdManager
4
+ import android.net.nsd.NsdServiceInfo
5
+ import kotlinx.coroutines.suspendCancellableCoroutine
6
+ import kotlin.coroutines.resume
7
+ import kotlin.coroutines.resumeWithException
8
+
9
+ @Suppress("DEPRECATION")
10
+ suspend fun NsdManager.resolveServiceCoroutine(serviceInfo: NsdServiceInfo): NsdServiceInfo =
11
+ suspendCancellableCoroutine { cont ->
12
+ resolveService(
13
+ serviceInfo,
14
+ object : NsdManager.ResolveListener {
15
+ override fun onResolveFailed(info: NsdServiceInfo, errorCode: Int) {
16
+ if (cont.isActive) {
17
+ cont.resumeWithException(RuntimeException("Resolve failed: errorCode=$errorCode"))
18
+ }
19
+ }
20
+
21
+ override fun onServiceResolved(resolvedInfo: NsdServiceInfo) {
22
+ if (cont.isActive) {
23
+ cont.resume(resolvedInfo)
24
+ }
25
+ }
26
+ }
27
+ )
28
+ }
@@ -0,0 +1,49 @@
1
+ package {{PACKAGE_NAME}}
2
+
3
+ import android.content.Context
4
+ import android.net.Uri
5
+ import android.os.Handler
6
+ import android.os.Looper
7
+ import okhttp3.OkHttpClient
8
+ import okhttp3.Request
9
+ import okhttp3.Response
10
+ import okhttp3.WebSocket
11
+ import okhttp3.WebSocketListener
12
+
13
+ class DevClientManager(private val context: Context, private val onReload: Runnable) {
14
+ private var webSocket: WebSocket? = null
15
+ private val handler = Handler(Looper.getMainLooper())
16
+ private val client = OkHttpClient.Builder()
17
+ .connectTimeout(5, java.util.concurrent.TimeUnit.SECONDS)
18
+ .readTimeout(0, java.util.concurrent.TimeUnit.SECONDS)
19
+ .build()
20
+
21
+ fun connect() {
22
+ val devUrl = DevServerPrefs.getUrl(context) ?: return
23
+ val uri = Uri.parse(devUrl)
24
+ val scheme = if (uri.scheme == "https") "wss" else "ws"
25
+ val host = uri.host ?: return
26
+ val port = if (uri.port > 0) ":${uri.port}" else ""
27
+ val path = (uri.path ?: "").let { p -> (if (p.endsWith("/")) p else p + "/") + "__hmr" }
28
+ val wsUrl = "$scheme://$host$port$path"
29
+ val request = Request.Builder()
30
+ .url(wsUrl)
31
+ .build()
32
+ webSocket = client.newWebSocket(request, object : WebSocketListener() {
33
+ override fun onMessage(webSocket: WebSocket, text: String) {
34
+ try {
35
+ if (text.contains("\"type\":\"reload\"")) {
36
+ handler.post(onReload)
37
+ }
38
+ } catch (_: Exception) { }
39
+ }
40
+ override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { }
41
+ override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { }
42
+ })
43
+ }
44
+
45
+ fun disconnect() {
46
+ webSocket?.close(1000, null)
47
+ webSocket = null
48
+ }
49
+ }
@@ -0,0 +1,44 @@
1
+ package {{PACKAGE_NAME}}
2
+
3
+ import android.content.Context
4
+ import android.content.SharedPreferences
5
+ import org.json.JSONArray
6
+
7
+ object DevServerPrefs {
8
+ private const val PREFS = "tamer_dev_server"
9
+ private const val KEY_URL = "dev_server_url"
10
+ private const val KEY_RECENT = "dev_server_recent"
11
+
12
+ fun getUrl(context: Context): String? {
13
+ return prefs(context).getString(KEY_URL, null)
14
+ }
15
+
16
+ fun setUrl(context: Context, url: String) {
17
+ prefs(context).edit().putString(KEY_URL, url).apply()
18
+ addRecent(context, url)
19
+ }
20
+
21
+ fun getRecentUrls(context: Context): List<String> {
22
+ val json = prefs(context).getString(KEY_RECENT, "[]") ?: "[]"
23
+ return try {
24
+ val arr = JSONArray(json)
25
+ (0 until arr.length()).map { arr.getString(it) }.distinct().take(10)
26
+ } catch (_: Exception) { emptyList() }
27
+ }
28
+
29
+ fun addRecent(context: Context, url: String) {
30
+ val current = getRecentUrls(context).filter { it != url }
31
+ val updated = listOf(url) + current
32
+ prefs(context).edit()
33
+ .putString(KEY_RECENT, JSONArray(updated.take(10)).toString())
34
+ .apply()
35
+ }
36
+
37
+ fun clear(context: Context) {
38
+ prefs(context).edit().clear().apply()
39
+ }
40
+
41
+ private fun prefs(context: Context): SharedPreferences {
42
+ return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
43
+ }
44
+ }
@@ -0,0 +1,5 @@
1
+ package {{PACKAGE_NAME}}
2
+
3
+ import com.journeyapps.barcodescanner.CaptureActivity
4
+
5
+ class PortraitCaptureActivity : CaptureActivity()
@@ -0,0 +1,112 @@
1
+ package {{PACKAGE_NAME}}
2
+
3
+ import android.content.Intent
4
+ import android.os.Bundle
5
+ import android.os.Handler
6
+ import android.os.Looper
7
+ import android.view.MotionEvent
8
+ import android.view.inputmethod.InputMethodManager
9
+ import android.widget.EditText
10
+ import androidx.appcompat.app.AppCompatActivity
11
+ import androidx.core.view.WindowCompat
12
+ import androidx.core.view.WindowInsetsControllerCompat
13
+ import com.lynx.tasm.LynxView
14
+ import com.lynx.tasm.LynxViewBuilder
15
+ import {{PACKAGE_NAME}}.DevClientManager
16
+ import {{PACKAGE_NAME}}.generated.GeneratedLynxExtensions
17
+ import {{PACKAGE_NAME}}.generated.GeneratedActivityLifecycle
18
+
19
+ class ProjectActivity : AppCompatActivity() {
20
+ private var lynxView: LynxView? = null
21
+ private var devClientManager: DevClientManager? = null
22
+ private val handler = Handler(Looper.getMainLooper())
23
+
24
+ override fun onCreate(savedInstanceState: Bundle?) {
25
+ super.onCreate(savedInstanceState)
26
+ GeneratedActivityLifecycle.onCreate(intent)
27
+ WindowCompat.setDecorFitsSystemWindows(window, false)
28
+ WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars = true
29
+ lynxView = buildLynxView()
30
+ setContentView(lynxView)
31
+ GeneratedActivityLifecycle.onViewAttached(lynxView)
32
+ GeneratedLynxExtensions.onHostViewChanged(lynxView)
33
+ lynxView?.renderTemplateUrl("main.lynx.bundle", "")
34
+ devClientManager = DevClientManager(this) { reloadProjectView() }
35
+ devClientManager?.connect()
36
+ GeneratedActivityLifecycle.onCreateDelayed(handler)
37
+ }
38
+
39
+ private fun reloadProjectView() {
40
+ GeneratedActivityLifecycle.onViewDetached()
41
+ GeneratedLynxExtensions.onHostViewChanged(null)
42
+ lynxView?.destroy()
43
+
44
+ val nextView = buildLynxView()
45
+ lynxView = nextView
46
+ setContentView(nextView)
47
+ GeneratedActivityLifecycle.onViewAttached(nextView)
48
+ GeneratedLynxExtensions.onHostViewChanged(nextView)
49
+ nextView.renderTemplateUrl("main.lynx.bundle", "")
50
+ GeneratedActivityLifecycle.onCreateDelayed(handler)
51
+ }
52
+
53
+ override fun onWindowFocusChanged(hasFocus: Boolean) {
54
+ super.onWindowFocusChanged(hasFocus)
55
+ GeneratedActivityLifecycle.onWindowFocusChanged(hasFocus)
56
+ }
57
+
58
+ override fun onResume() {
59
+ super.onResume()
60
+ GeneratedActivityLifecycle.onResume()
61
+ }
62
+
63
+ override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
64
+ if (ev.action == MotionEvent.ACTION_DOWN) maybeClearFocusedInput(ev)
65
+ return super.dispatchTouchEvent(ev)
66
+ }
67
+
68
+ private fun maybeClearFocusedInput(ev: MotionEvent) {
69
+ val focused = currentFocus
70
+ if (focused is EditText) {
71
+ val loc = IntArray(2)
72
+ focused.getLocationOnScreen(loc)
73
+ val x = ev.rawX.toInt()
74
+ val y = ev.rawY.toInt()
75
+ if (x < loc[0] || x > loc[0] + focused.width || y < loc[1] || y > loc[1] + focused.height) {
76
+ focused.clearFocus()
77
+ (getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager)
78
+ ?.hideSoftInputFromWindow(focused.windowToken, 0)
79
+ }
80
+ }
81
+ }
82
+
83
+ override fun onNewIntent(intent: Intent) {
84
+ super.onNewIntent(intent)
85
+ setIntent(intent)
86
+ GeneratedActivityLifecycle.onNewIntent(intent)
87
+ }
88
+
89
+ @Deprecated("Deprecated in Java")
90
+ override fun onBackPressed() {
91
+ GeneratedActivityLifecycle.onBackPressed { consumed ->
92
+ if (!consumed) {
93
+ runOnUiThread { super.onBackPressed() }
94
+ }
95
+ }
96
+ }
97
+
98
+ override fun onDestroy() {
99
+ GeneratedActivityLifecycle.onViewDetached()
100
+ GeneratedLynxExtensions.onHostViewChanged(null)
101
+ lynxView?.destroy()
102
+ lynxView = null
103
+ devClientManager?.disconnect()
104
+ super.onDestroy()
105
+ }
106
+
107
+ private fun buildLynxView(): LynxView {
108
+ val viewBuilder = LynxViewBuilder()
109
+ viewBuilder.setTemplateProvider(TemplateProvider(this))
110
+ return viewBuilder.build(this)
111
+ }
112
+ }
Binary file
@@ -0,0 +1,298 @@
1
+ import Foundation
2
+ import Lynx
3
+ import AVFoundation
4
+
5
+ private final class BonjourResolver: NSObject, NetServiceBrowserDelegate, NetServiceDelegate {
6
+ var onServersChanged: (([[String: String]]) -> Void)?
7
+
8
+ private let browser = NetServiceBrowser()
9
+ private var services: [String: NetService] = [:]
10
+ private var resolved: [String: [String: String]] = [:]
11
+
12
+ override init() {
13
+ super.init()
14
+ browser.delegate = self
15
+ }
16
+
17
+ func start() {
18
+ browser.schedule(in: .main, forMode: .common)
19
+ browser.searchForServices(ofType: "_tamer._tcp.", inDomain: "local.")
20
+ }
21
+
22
+ func stop() {
23
+ browser.stop()
24
+ browser.remove(from: .main, forMode: .common)
25
+ services.values.forEach { service in
26
+ service.stop()
27
+ service.remove(from: .main, forMode: .common)
28
+ service.delegate = nil
29
+ }
30
+ services.removeAll()
31
+ resolved.removeAll()
32
+ onServersChanged?([])
33
+ }
34
+
35
+ func netServiceBrowserWillSearch(_ browser: NetServiceBrowser) {
36
+ onServersChanged?(Array(resolved.values))
37
+ }
38
+
39
+ func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) {
40
+ let key = serviceKey(service)
41
+ service.delegate = self
42
+ service.schedule(in: .main, forMode: .common)
43
+ services[key] = service
44
+ service.resolve(withTimeout: 10)
45
+ if !moreComing {
46
+ onServersChanged?(sortedServers())
47
+ }
48
+ }
49
+
50
+ func netServiceBrowser(_ browser: NetServiceBrowser, didRemove service: NetService, moreComing: Bool) {
51
+ let key = serviceKey(service)
52
+ services[key]?.stop()
53
+ services[key]?.delegate = nil
54
+ services.removeValue(forKey: key)
55
+ resolved.removeValue(forKey: key)
56
+ if !moreComing {
57
+ onServersChanged?(sortedServers())
58
+ }
59
+ }
60
+
61
+ func netServiceDidResolveAddress(_ sender: NetService) {
62
+ let key = serviceKey(sender)
63
+ guard let host = sender.hostName?.trimmingCharacters(in: CharacterSet(charactersIn: ".")),
64
+ !host.isEmpty,
65
+ sender.port > 0 else {
66
+ return
67
+ }
68
+ resolved[key] = [
69
+ "url": "http://\(host):\(sender.port)",
70
+ "name": sender.name
71
+ ]
72
+ onServersChanged?(sortedServers())
73
+ }
74
+
75
+ func netService(_ sender: NetService, didNotResolve errorDict: [String : NSNumber]) {
76
+ resolved.removeValue(forKey: serviceKey(sender))
77
+ onServersChanged?(sortedServers())
78
+ }
79
+
80
+ private func serviceKey(_ service: NetService) -> String {
81
+ "\(service.name)|\(service.type)|\(service.domain)"
82
+ }
83
+
84
+ private func sortedServers() -> [[String: String]] {
85
+ resolved.values.sorted { lhs, rhs in
86
+ (lhs["name"] ?? lhs["url"] ?? "") < (rhs["name"] ?? rhs["url"] ?? "")
87
+ }
88
+ }
89
+ }
90
+
91
+ @objcMembers
92
+ public final class DevClientModule: NSObject, LynxModule {
93
+
94
+ // MARK: - LynxModule Protocol
95
+
96
+ @objc public static var name: String { "DevClientModule" }
97
+
98
+ @objc public static var methodLookup: [String: String] {
99
+ [
100
+ "getDevServerUrl": NSStringFromSelector(#selector(getDevServerUrl(_:))),
101
+ "setDevServerUrl": NSStringFromSelector(#selector(setDevServerUrl(_:))),
102
+ "getRecentUrls": NSStringFromSelector(#selector(getRecentUrls(_:))),
103
+ "clearDevServerUrl": NSStringFromSelector(#selector(clearDevServerUrl)),
104
+ "scanQR": NSStringFromSelector(#selector(scanQR)),
105
+ "reloadWithProjectBundle": NSStringFromSelector(#selector(reloadWithProjectBundle)),
106
+ "startDiscovery": NSStringFromSelector(#selector(startDiscovery)),
107
+ "stopDiscovery": NSStringFromSelector(#selector(stopDiscovery)),
108
+ "getDiscoveredServers": NSStringFromSelector(#selector(getDiscoveredServers(_:))),
109
+ "checkServerCompatibility": NSStringFromSelector(#selector(checkServerCompatibility(_:callback:))),
110
+ ]
111
+ }
112
+
113
+ // MARK: - Static Attachment Points (set by DevLauncherViewController)
114
+
115
+ public static weak var shared: DevClientModule?
116
+
117
+ /// Present a QR scanner; call completion with scanned URL string or nil on cancel.
118
+ public static var presentQRScanner: ((@escaping (String?) -> Void) -> Void)?
119
+
120
+ /// Navigate to the project view controller.
121
+ public static var reloadProjectHandler: (() -> Void)?
122
+
123
+ // MARK: - Instance State
124
+
125
+ private weak var lynxContext: LynxContext?
126
+ private var bonjourResolver: BonjourResolver?
127
+ private var lastDiscovered: [[String: String]] = []
128
+
129
+ // MARK: - Init
130
+
131
+ @objc public required init(param: Any) {
132
+ super.init()
133
+ lynxContext = param as? LynxContext
134
+ DevClientModule.shared = self
135
+ }
136
+
137
+ @objc public override init() {
138
+ super.init()
139
+ DevClientModule.shared = self
140
+ }
141
+
142
+ // MARK: - LynxModule Methods
143
+
144
+ @objc func getDevServerUrl(_ callback: LynxCallbackBlock) {
145
+ callback(DevServerPrefs.getUrl() as Any)
146
+ }
147
+
148
+ @objc func setDevServerUrl(_ url: String) {
149
+ var normalized = url.trimmingCharacters(in: .whitespaces)
150
+ if normalized.hasSuffix("/main.lynx.bundle") {
151
+ normalized = String(normalized.dropLast("/main.lynx.bundle".count))
152
+ } else if normalized.hasSuffix("main.lynx.bundle") {
153
+ normalized = String(normalized.dropLast("main.lynx.bundle".count))
154
+ .trimmingCharacters(in: CharacterSet(charactersIn: "/"))
155
+ }
156
+ DevServerPrefs.setUrl(normalized)
157
+ }
158
+
159
+ @objc func getRecentUrls(_ callback: LynxCallbackBlock) {
160
+ callback(DevServerPrefs.getRecentUrls() as NSArray)
161
+ }
162
+
163
+ @objc func clearDevServerUrl() {
164
+ DevServerPrefs.clear()
165
+ }
166
+
167
+ @objc func scanQR() {
168
+ DispatchQueue.main.async { [weak self] in
169
+ guard let self = self else { return }
170
+ guard let present = DevClientModule.presentQRScanner else {
171
+ self.emitScanResult(nil)
172
+ return
173
+ }
174
+ present { [weak self] result in
175
+ self?.emitScanResult(result)
176
+ }
177
+ }
178
+ }
179
+
180
+ @objc func reloadWithProjectBundle() {
181
+ DispatchQueue.main.async {
182
+ DevClientModule.reloadProjectHandler?()
183
+ }
184
+ }
185
+
186
+ @objc func startDiscovery() {
187
+ guard bonjourResolver == nil else { return }
188
+ let resolver = BonjourResolver()
189
+ resolver.onServersChanged = { [weak self] servers in
190
+ self?.emitDiscoveredServers(servers)
191
+ }
192
+ bonjourResolver = resolver
193
+ DispatchQueue.main.async {
194
+ resolver.start()
195
+ }
196
+ }
197
+
198
+ @objc func stopDiscovery() {
199
+ bonjourResolver?.stop()
200
+ bonjourResolver = nil
201
+ }
202
+
203
+ @objc func getDiscoveredServers(_ callback: LynxCallbackBlock) {
204
+ let list = lastDiscovered.map { s -> NSDictionary in
205
+ ["url": s["url"] ?? "", "name": s["name"] ?? ""] as NSDictionary
206
+ }
207
+ callback(list as NSArray)
208
+ }
209
+
210
+ @objc func checkServerCompatibility(_ baseUrl: String, callback: @escaping LynxCallbackBlock) {
211
+ DispatchQueue.global(qos: .utility).async {
212
+ let trimmed = baseUrl.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
213
+ guard let url = URL(string: trimmed + "/meta.json") else {
214
+ callback([true, []] as NSArray)
215
+ return
216
+ }
217
+ var req = URLRequest(url: url)
218
+ req.timeoutInterval = 5
219
+ URLSession.shared.dataTask(with: req) { data, _, _ in
220
+ guard let data = data,
221
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
222
+ let modules = json["nativeModules"] as? [[String: Any]] else {
223
+ callback([true, []] as NSArray)
224
+ return
225
+ }
226
+ let required = modules.compactMap { $0["moduleClassName"] as? String }
227
+ callback([true, required] as NSArray)
228
+ }.resume()
229
+ }
230
+ }
231
+
232
+ // MARK: - Private
233
+
234
+ private func emitScanResult(_ url: String?) {
235
+ let payload = url.map { "{\"url\":\"\($0)\"}" } ?? "{\"url\":\"\"}"
236
+ sendEvent("devclient:scanResult", payload: payload)
237
+ }
238
+
239
+ private func emitDiscoveredServers(_ servers: [[String: String]]) {
240
+ lastDiscovered = servers
241
+ let payloadServers = servers.map { server -> [String: Any] in
242
+ [
243
+ "url": server["url"] ?? "",
244
+ "name": server["name"] ?? ""
245
+ ]
246
+ }
247
+ let payload = (try? JSONSerialization.data(withJSONObject: ["servers": payloadServers]))
248
+ .flatMap { String(data: $0, encoding: .utf8) } ?? "{\"servers\":[]}"
249
+ sendEvent("devclient:discoveredServers", payload: payload)
250
+ }
251
+
252
+ private func sendEvent(_ name: String, payload: String) {
253
+ let params: [[String: Any]] = [["payload": payload]]
254
+ if let ctx = lynxContext {
255
+ ctx.sendGlobalEvent(name, withParams: params)
256
+ } else if let ctx = DevClientModule.shared?.lynxContext {
257
+ ctx.sendGlobalEvent(name, withParams: params)
258
+ }
259
+ }
260
+ }
261
+
262
+ // MARK: - DevServerPrefs
263
+
264
+ public final class DevServerPrefs {
265
+ private static let suite = "tamer_dev_server"
266
+ private static let keyUrl = "dev_server_url"
267
+ private static let keyRecent = "dev_server_recent"
268
+ private static var defaults: UserDefaults { UserDefaults(suiteName: suite) ?? .standard }
269
+
270
+ public static func getUrl() -> String? {
271
+ defaults.string(forKey: keyUrl)
272
+ }
273
+
274
+ public static func setUrl(_ url: String) {
275
+ defaults.set(url, forKey: keyUrl)
276
+ addRecent(url)
277
+ }
278
+
279
+ public static func getRecentUrls() -> [String] {
280
+ guard let json = defaults.string(forKey: keyRecent),
281
+ let data = json.data(using: .utf8),
282
+ let arr = try? JSONDecoder().decode([String].self, from: data) else { return [] }
283
+ return Array(arr.prefix(10))
284
+ }
285
+
286
+ public static func addRecent(_ url: String) {
287
+ var current = getRecentUrls().filter { $0 != url }
288
+ current.insert(url, at: 0)
289
+ if let data = try? JSONEncoder().encode(Array(current.prefix(10))),
290
+ let json = String(data: data, encoding: .utf8) {
291
+ defaults.set(json, forKey: keyRecent)
292
+ }
293
+ }
294
+
295
+ public static func clear() {
296
+ defaults.removePersistentDomain(forName: suite)
297
+ }
298
+ }