also looking at this
fix(android): hand the launch link to the page that subscribes for it
#232
17 files
+776
-136
| @@ -221,6 +221,14 @@ class CraftBridge( | ||
| 221 | 221 | private var bridgeReady = false |
| 222 | 222 | private val pendingEvents = mutableListOf<Pair<String, Map<String, Any>>>() |
| 223 | 223 | |
| 224 | // Deep links (#215), on the UI thread only, the way iOS's DeepLinkManager | |
| 225 | // holds them: a link that arrives before the first page has its bridge is | |
| 226 | // the launch link, and every link waits for an injected bridge. | |
| 227 | private var hasBeenReady = false | |
| 228 | private var initialAssigned = false | |
| 229 | private var launchURL: String? = null | |
| 230 | private val pendingDeepLinks = mutableListOf<Pair<String, Boolean>>() | |
| 231 | ||
| 224 | 232 | // Location tracking |
| 225 | 233 | private var fusedLocationClient: FusedLocationProviderClient? = null |
| 226 | 234 | private val locationCallbacks = mutableMapOf<Int, LocationCallback>() |
| @@ -310,6 +318,10 @@ class CraftBridge( | ||
| 310 | 318 | // still loading has somewhere to go rather than being counted as |
| 311 | 319 | // dropped. |
| 312 | 320 | installNativeDeliverer() |
| 321 | // A page is about to exist, so no later link launched the app, even | |
| 322 | // one that arrives before this script's completion runs. iOS marks | |
| 323 | // the same moment. | |
| 324 | hasBeenReady = true | |
| 313 | 325 | |
| 314 | 326 | val script = """ |
| 315 | 327 | {{PROMISE_RUNTIME}} |
| @@ -598,7 +610,7 @@ class CraftBridge( | ||
| 598 | 610 | |
| 599 | 611 | // Deep Links |
| 600 | 612 | onDeepLink: function(callback) { |
| 601 | window.addEventListener('craftDeepLink', function(e) { callback(e.detail); }); | |
| 613 | return window.craft._subscribeDeepLinks(callback); | |
| 602 | 614 | }, |
| 603 | 615 | |
| 604 | 616 | // QR/Barcode Scanner |
| @@ -999,12 +1011,13 @@ class CraftBridge( | ||
| 999 | 1011 | // Deep Links |
| 1000 | 1012 | deepLinks: { |
| 1001 | 1013 | getInitialURL: function() { |
| 1014 | window.craft._claimInitialDeepLink(); | |
| 1002 | 1015 | return window.__craftPromise('initial URL', '_craftDeepLinkResolve', '_craftDeepLinkReject', function() { |
| 1003 | 1016 | CraftAndroid.getInitialURL(); |
| 1004 | 1017 | }); |
| 1005 | 1018 | }, |
| 1006 | 1019 | onLink: function(callback) { |
| 1007 | window.addEventListener('craftDeepLink', function(e) { callback(e.detail); }); | |
| 1020 | return window.craft._subscribeDeepLinks(callback); | |
| 1008 | 1021 | } |
| 1009 | 1022 | }, |
| 1010 | 1023 | |
| @@ -1354,6 +1367,62 @@ class CraftBridge( | ||
| 1354 | 1367 | } |
| 1355 | 1368 | }; |
| 1356 | 1369 | |
| 1370 | // Links that arrive before anything is listening (#215, as iOS | |
| 1371 | // does since #198). | |
| 1372 | // | |
| 1373 | // Native dispatches a link once this script has run, and on a cold | |
| 1374 | // start that is the link the app was opened with. Only a craftReady | |
| 1375 | // handler can have called onLink by then, since onLink is defined | |
| 1376 | // by this very script. So such a link used to reach nobody, and a | |
| 1377 | // page that subscribes later, rather than asking getInitialURL, | |
| 1378 | // never learned how it was opened. | |
| 1379 | // | |
| 1380 | // Held here instead, and handed to the first subscriber on the | |
| 1381 | // next turn, so an unsubscribe returned in the same tick still | |
| 1382 | // applies. The launch link belongs to getInitialURL once the page | |
| 1383 | // has called it, whether native has dispatched it yet or not, so a | |
| 1384 | // page that does both in the same tick or in a craftReady handler, | |
| 1385 | // in either order, gets it once. A page that asks getInitialURL | |
| 1386 | // only later, after an await, should skip `initial` in onLink. | |
| 1387 | // | |
| 1388 | // The state lives on window, not in this function, because this | |
| 1389 | // script can run twice in one document, and a second copy must | |
| 1390 | // neither reset it nor buffer every link a second time. | |
| 1391 | (function installDeepLinkReplay(craft) { | |
| 1392 | var replay = window.__craftDeepLinkReplay; | |
| 1393 | if (!replay) { | |
| 1394 | replay = window.__craftDeepLinkReplay = {undelivered: [], subscribed: false, initialClaimed: false}; | |
| 1395 | window.addEventListener('craftDeepLink', function(e) { | |
| 1396 | if (!replay.subscribed && !claimed(e.detail)) replay.undelivered.push(e.detail); | |
| 1397 | }); | |
| 1398 | } | |
| 1399 | function claimed(detail) { | |
| 1400 | return replay.initialClaimed && detail && detail.initial; | |
| 1401 | } | |
| 1402 | craft._subscribeDeepLinks = function(callback) { | |
| 1403 | var active = true; | |
| 1404 | var listener = function(e) { if (!claimed(e.detail)) callback(e.detail); }; | |
| 1405 | window.addEventListener('craftDeepLink', listener); | |
| 1406 | if (!replay.subscribed) { | |
| 1407 | replay.subscribed = true; | |
| 1408 | setTimeout(function() { | |
| 1409 | var pending = replay.undelivered; | |
| 1410 | replay.undelivered = []; | |
| 1411 | if (!active) return; | |
| 1412 | pending.forEach(function(detail) { callback(detail); }); | |
| 1413 | }, 0); | |
| 1414 | } | |
| 1415 | return function() { | |
| 1416 | active = false; | |
| 1417 | window.removeEventListener('craftDeepLink', listener); | |
| 1418 | }; | |
| 1419 | }; | |
| 1420 | craft._claimInitialDeepLink = function() { | |
| 1421 | replay.initialClaimed = true; | |
| 1422 | replay.undelivered = replay.undelivered.filter(function(detail) { return !(detail && detail.initial); }); | |
| 1423 | }; | |
| 1424 | })(window.craft); | |
| 1425 | ||
| 1357 | 1426 | (function installCraftMobileContract(craft) { |
| 1358 | 1427 | var legacyShare = craft.share.bind(craft); |
| 1359 | 1428 | var legacyOpenCamera = craft.openCamera.bind(craft); |
| @@ -1485,6 +1554,11 @@ class CraftBridge( | ||
| 1485 | 1554 | val events = pendingEvents.toList() |
| 1486 | 1555 | pendingEvents.clear() |
| 1487 | 1556 | events.forEach { (name, data) -> sendEvent(name, data) } |
| 1557 | // After the script, so the page's replay listener exists and | |
| 1558 | // the native deliverer is installed. | |
| 1559 | val links = pendingDeepLinks.toList() | |
| 1560 | pendingDeepLinks.clear() | |
| 1561 | links.forEach { (url, initial) -> dispatchDeepLink(url, initial) } | |
| 1488 | 1562 | } |
| 1489 | 1563 | } |
| 1490 | 1564 | } |
| @@ -3314,18 +3388,6 @@ class CraftBridge( | ||
| 3314 | 3388 | return true |
| 3315 | 3389 | } |
| 3316 | 3390 | |
| 3317 | // ==================== Deep Links ==================== | |
| 3318 | ||
| 3319 | fun handleDeepLink(uri: android.net.Uri) { | |
| 3320 | sendEvent("craftDeepLink", mapOf( | |
| 3321 | "url" to uri.toString(), | |
| 3322 | "scheme" to (uri.scheme ?: ""), | |
| 3323 | "host" to (uri.host ?: ""), | |
| 3324 | "path" to (uri.path ?: ""), | |
| 3325 | "query" to (uri.query ?: "") | |
| 3326 | )) | |
| 3327 | } | |
| 3328 | ||
| 3329 | 3391 | // ==================== QR/Barcode Scanner ==================== |
| 3330 | 3392 | |
| 3331 | 3393 | @JavascriptInterface |
| @@ -3889,6 +3951,7 @@ class CraftBridge( | ||
| 3889 | 3951 | isKeepingAwake = false |
| 3890 | 3952 | pendingPermissionRequests.clear() |
| 3891 | 3953 | pendingEvents.clear() |
| 3954 | pendingDeepLinks.clear() | |
| 3892 | 3955 | } |
| 3893 | 3956 | |
| 3894 | 3957 | // ==================== Screen Capture ==================== |
| @@ -4662,13 +4725,44 @@ class CraftBridge( | ||
| 4662 | 4725 | } |
| 4663 | 4726 | } |
| 4664 | 4727 | |
| 4665 | fun dispatchDeepLink(url: String) { | |
| 4666 | if (nativeDeepLinks && CraftNative.dispatchDeepLink(url)) return | |
| 4667 | ||
| 4668 | // Store as initial URL if this is the first one | |
| 4669 | if (initialURL == null) { | |
| 4670 | initialURL = url | |
| 4728 | /** | |
| 4729 | * A link the activity received. The first one before any page was ready | |
| 4730 | * launched the app: it becomes getInitialURL's answer and is dispatched | |
| 4731 | * with `initial: true`. Every later link is `initial: false` and leaves | |
| 4732 | * getInitialURL alone, as on iOS. | |
| 4733 | */ | |
| 4734 | fun receiveDeepLink(url: String) { | |
| 4735 | val initial = !hasBeenReady && !initialAssigned | |
| 4736 | if (initial) { | |
| 4737 | initialAssigned = true | |
| 4738 | launchURL = url | |
| 4739 | setInitialURL(url) | |
| 4671 | 4740 | } |
| 4741 | if (bridgeReady) dispatchDeepLink(url, false) else pendingDeepLinks.add(url to initial) | |
| 4742 | } | |
| 4743 | ||
| 4744 | fun saveDeepLinks(outState: Bundle) { | |
| 4745 | outState.putBoolean(STATE_LAUNCH_ASSIGNED, initialAssigned) | |
| 4746 | outState.putBoolean(STATE_PAGE_WAS_READY, hasBeenReady) | |
| 4747 | outState.putString(STATE_LAUNCH_URL, launchURL) | |
| 4748 | } | |
| 4749 | ||
| 4750 | /** | |
| 4751 | * A recreated activity loads a new WebView and a fresh document, so the | |
| 4752 | * launch link is queued again for that document's first subscriber, the | |
| 4753 | * way it was for the first one; a page that called getInitialURL claims | |
| 4754 | * it as before. Links that arrive now are never the launch link. | |
| 4755 | */ | |
| 4756 | fun restoreDeepLinks(savedState: Bundle) { | |
| 4757 | initialAssigned = savedState.getBoolean(STATE_LAUNCH_ASSIGNED, true) | |
| 4758 | hasBeenReady = savedState.getBoolean(STATE_PAGE_WAS_READY, true) | |
| 4759 | launchURL = savedState.getString(STATE_LAUNCH_URL) | |
| 4760 | setInitialURL(launchURL) | |
| 4761 | launchURL?.let { pendingDeepLinks.add(it to true) } | |
| 4762 | } | |
| 4763 | ||
| 4764 | private fun dispatchDeepLink(url: String, initial: Boolean) { | |
| 4765 | if (nativeDeepLinks && CraftNative.dispatchDeepLink(url, initial)) return | |
| 4672 | 4766 | |
| 4673 | 4767 | val uri = Uri.parse(url) |
| 4674 | 4768 | val json = JSONObject().apply { |
| @@ -4683,6 +4777,7 @@ class CraftBridge( | ||
| 4683 | 4777 | queryParams.put(name, uri.getQueryParameter(name) ?: "") |
| 4684 | 4778 | } |
| 4685 | 4779 | put("queryParams", queryParams) |
| 4780 | put("initial", initial) | |
| 4686 | 4781 | } |
| 4687 | 4782 | |
| 4688 | 4783 | evaluateJavascriptUnlessClosed( |
| @@ -4753,6 +4848,9 @@ class CraftBridge( | ||
| 4753 | 4848 | } |
| 4754 | 4849 | |
| 4755 | 4850 | companion object { |
| 4851 | private const val STATE_LAUNCH_ASSIGNED = "craft.deepLinks.launchAssigned" | |
| 4852 | private const val STATE_PAGE_WAS_READY = "craft.deepLinks.pageWasReady" | |
| 4853 | private const val STATE_LAUNCH_URL = "craft.deepLinks.launchURL" | |
| 4756 | 4854 | private val MIME_TYPE = Regex("^[^\\s/]+/[^\\s/]+$") |
| 4757 | 4855 | const val REQUEST_CAMERA = 1001 |
| 4758 | 4856 | const val REQUEST_GALLERY = 1002 |