@apex-inc/capacitor-plugin 2.2.5 → 2.2.6

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.
@@ -14,6 +14,7 @@ import com.getcapacitor.PluginMethod
14
14
  import com.getcapacitor.annotation.CapacitorPlugin
15
15
  import com.google.android.gms.ads.identifier.AdvertisingIdClient
16
16
  import com.google.firebase.messaging.FirebaseMessaging
17
+ import org.json.JSONObject
17
18
  import java.io.File
18
19
  import java.util.Locale
19
20
  import java.util.TimeZone
@@ -373,7 +374,7 @@ class ApexCapacitorPlugin : Plugin() {
373
374
 
374
375
  @PluginMethod
375
376
  fun getInstallReferrer(call: PluginCall) {
376
- val data = JSObject().put("referrer", lastInstallReferrer ?: JSONull())
377
+ val data = JSObject().put("referrer", lastInstallReferrer ?: JSONObject.NULL)
377
378
  call.resolve(data)
378
379
  }
379
380
 
@@ -484,32 +485,103 @@ class ApexCapacitorPlugin : Plugin() {
484
485
  val commitSha = call.getString("commitSha")
485
486
 
486
487
  // Bitmap the activity's decor view on the main thread, JPEG-encode
487
- // + base64, then POST off the main thread.
488
+ // + base64, then POST off the main thread. PixelCopy (API 26+) is
489
+ // the correct way to snapshot a hardware-accelerated window — it
490
+ // copies the actual rendered surface, so SurfaceView / video /
491
+ // GL content is captured rather than coming out black. Older API
492
+ // levels fall back to the software `view.draw(Canvas)` path.
488
493
  activity.runOnUiThread {
489
494
  try {
490
- // TODO(native-unverified): decor-view bitmap capture cannot
491
- // be compiled/verified in this environment.
492
495
  val decor = activity.window.decorView
496
+ val width = decor.width.coerceAtLeast(1)
497
+ val height = decor.height.coerceAtLeast(1)
493
498
  val bitmap = android.graphics.Bitmap.createBitmap(
494
- decor.width.coerceAtLeast(1),
495
- decor.height.coerceAtLeast(1),
499
+ width,
500
+ height,
496
501
  android.graphics.Bitmap.Config.ARGB_8888,
497
502
  )
498
- val canvas = android.graphics.Canvas(bitmap)
499
- decor.draw(canvas)
500
- val stream = java.io.ByteArrayOutputStream()
501
- bitmap.compress(android.graphics.Bitmap.CompressFormat.JPEG, 70, stream)
502
- val imageBase64 = android.util.Base64.encodeToString(
503
- stream.toByteArray(),
504
- android.util.Base64.NO_WRAP,
505
- )
506
- uploadVariantScreenshot(call, experimentId, variantKey, imageBase64, label, commitSha)
503
+
504
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
505
+ val location = IntArray(2)
506
+ decor.getLocationInWindow(location)
507
+ val rect = android.graphics.Rect(
508
+ location[0],
509
+ location[1],
510
+ location[0] + width,
511
+ location[1] + height,
512
+ )
513
+ try {
514
+ android.view.PixelCopy.request(
515
+ activity.window,
516
+ rect,
517
+ bitmap,
518
+ { copyResult ->
519
+ if (copyResult == android.view.PixelCopy.SUCCESS) {
520
+ encodeAndUploadScreenshot(
521
+ call, experimentId, variantKey, bitmap, label, commitSha,
522
+ )
523
+ } else {
524
+ // PixelCopy refused (e.g. window not
525
+ // backed by a surface yet) — fall back
526
+ // to the software draw path.
527
+ drawViewToBitmap(decor, bitmap)
528
+ encodeAndUploadScreenshot(
529
+ call, experimentId, variantKey, bitmap, label, commitSha,
530
+ )
531
+ }
532
+ },
533
+ android.os.Handler(android.os.Looper.getMainLooper()),
534
+ )
535
+ } catch (e: IllegalArgumentException) {
536
+ // PixelCopy throws if the source isn't ready — software fallback.
537
+ drawViewToBitmap(decor, bitmap)
538
+ encodeAndUploadScreenshot(
539
+ call, experimentId, variantKey, bitmap, label, commitSha,
540
+ )
541
+ }
542
+ } else {
543
+ drawViewToBitmap(decor, bitmap)
544
+ encodeAndUploadScreenshot(
545
+ call, experimentId, variantKey, bitmap, label, commitSha,
546
+ )
547
+ }
507
548
  } catch (e: Exception) {
508
549
  call.resolve(JSObject().put("captured", false).put("reason", "encode_failed"))
509
550
  }
510
551
  }
511
552
  }
512
553
 
554
+ /** Software-rasterize a view's current content into [bitmap]. */
555
+ private fun drawViewToBitmap(view: android.view.View, bitmap: android.graphics.Bitmap) {
556
+ val canvas = android.graphics.Canvas(bitmap)
557
+ view.draw(canvas)
558
+ }
559
+
560
+ /**
561
+ * JPEG-encode + base64 a captured bitmap and hand it off to the
562
+ * background uploader. Resolves `encode_failed` if compression throws.
563
+ */
564
+ private fun encodeAndUploadScreenshot(
565
+ call: PluginCall,
566
+ experimentId: String,
567
+ variantKey: String,
568
+ bitmap: android.graphics.Bitmap,
569
+ label: String?,
570
+ commitSha: String?,
571
+ ) {
572
+ try {
573
+ val stream = java.io.ByteArrayOutputStream()
574
+ bitmap.compress(android.graphics.Bitmap.CompressFormat.JPEG, 70, stream)
575
+ val imageBase64 = android.util.Base64.encodeToString(
576
+ stream.toByteArray(),
577
+ android.util.Base64.NO_WRAP,
578
+ )
579
+ uploadVariantScreenshot(call, experimentId, variantKey, imageBase64, label, commitSha)
580
+ } catch (e: Exception) {
581
+ call.resolve(JSObject().put("captured", false).put("reason", "encode_failed"))
582
+ }
583
+ }
584
+
513
585
  /**
514
586
  * POST a captured screenshot to `${apiBaseUrl}/api/experiments/:id/
515
587
  * assets` with `source: "device"`. Networking mirrors `identify()`.
@@ -590,7 +662,7 @@ class ApexCapacitorPlugin : Plugin() {
590
662
  @PluginMethod
591
663
  fun getInitialDeepLink(call: PluginCall) {
592
664
  val url = deepLinks.consumeInitialUrl()
593
- val data = JSObject().put("url", url ?: JSONull())
665
+ val data = JSObject().put("url", url ?: JSONObject.NULL)
594
666
  call.resolve(data)
595
667
  }
596
668
 
@@ -639,11 +711,11 @@ class ApexCapacitorPlugin : Plugin() {
639
711
  fun getCurrentSession(call: PluginCall) {
640
712
  val current = sessionManager.getCurrent()
641
713
  val data = JSObject()
642
- .put("sessionId", current?.sessionId ?: JSONull())
714
+ .put("sessionId", current?.sessionId ?: JSONObject.NULL)
643
715
  .put(
644
716
  "startedAt",
645
717
  current?.startedAtMs?.let { java.time.Instant.ofEpochMilli(it).toString() }
646
- ?: JSONull()
718
+ ?: JSONObject.NULL
647
719
  )
648
720
  call.resolve(data)
649
721
  }
@@ -705,14 +777,14 @@ class ApexCapacitorPlugin : Plugin() {
705
777
  fun getQueueSize(call: PluginCall) {
706
778
  val queue = offlineQueue
707
779
  if (queue == null) {
708
- call.resolve(JSObject().put("count", 0).put("oldestEventAt", JSONull()))
780
+ call.resolve(JSObject().put("count", 0).put("oldestEventAt", JSONObject.NULL))
709
781
  return
710
782
  }
711
783
  val oldest = queue.oldestEventAtMs()
712
784
  ?.let { java.time.Instant.ofEpochMilli(it).toString() }
713
785
  val data = JSObject()
714
786
  .put("count", queue.size())
715
- .put("oldestEventAt", oldest ?: JSONull())
787
+ .put("oldestEventAt", oldest ?: JSONObject.NULL)
716
788
  call.resolve(data)
717
789
  }
718
790
 
@@ -839,8 +911,3 @@ private class FileQueueStorage(private val file: File) : QueueStorage {
839
911
  file.writeText(contents)
840
912
  }
841
913
  }
842
-
843
- /** Sentinel for passing JSON null to Capacitor's JSObject helpers. */
844
- private object JSONull {
845
- override fun toString(): String = "null"
846
- }
@@ -686,9 +686,25 @@ public class ApexCapacitorPlugin: CAPPlugin, CAPBridgedPlugin, UNUserNotificatio
686
686
  // UIKit snapshot API.
687
687
  DispatchQueue.main.async { [weak self] in
688
688
  guard let self = self else { return }
689
- // TODO(native-unverified): key-window discovery + renderer
690
- // cannot be compiled/verified in this environment.
691
- guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) ?? UIApplication.shared.windows.first,
689
+ // Modern key-window discovery: walk the connected scenes for
690
+ // the foreground-active UIWindowScene, then take its key
691
+ // window. `UIApplication.shared.windows` /
692
+ // `UIApplication.shared.keyWindow` are deprecated as of iOS
693
+ // 15; scanning scene windows works on the iOS 13+ deployment
694
+ // target without tripping the deprecation.
695
+ let windowScene = UIApplication.shared.connectedScenes
696
+ .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene
697
+ ?? UIApplication.shared.connectedScenes.first(where: { $0 is UIWindowScene }) as? UIWindowScene
698
+ let keyWindow: UIWindow?
699
+ if #available(iOS 15.0, *) {
700
+ keyWindow = windowScene?.keyWindow
701
+ ?? windowScene?.windows.first(where: { $0.isKeyWindow })
702
+ ?? windowScene?.windows.first
703
+ } else {
704
+ keyWindow = windowScene?.windows.first(where: { $0.isKeyWindow })
705
+ ?? windowScene?.windows.first
706
+ }
707
+ guard let window = keyWindow,
692
708
  let rootView = window.rootViewController?.view else {
693
709
  call.resolve(["captured": false, "reason": "no_window"])
694
710
  return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apex-inc/capacitor-plugin",
3
- "version": "2.2.5",
3
+ "version": "2.2.6",
4
4
  "description": "Apex Capacitor plugin — iOS/Android attribution, events, deep linking, SKAN, and offline-tolerant tracking for Capacitor apps.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/esm/index.js",