Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ app registered `myapp`, if one is installed. The returned `success` is the real
answer: `false` means the system found nothing to open it with, which is how you
detect that the target app is not installed.

A web link that another app handles (YouTube, Maps) opens in that app. A link your
own app handles (one on your `NATIVEPHP_DEEPLINK_HOST`) opens in the browser rather
than coming back into your app. `inApp()` and `auth()` do the same on Android.

### `auth()`

Opens a URL in a specialized authentication browser designed for OAuth flows with automatic `nativephp://` redirect handling.
Expand Down
6 changes: 5 additions & 1 deletion resources/android/AuthTab.kt
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,11 @@ class AuthTabActivity : Activity() {
tabOpened = true

try {
CustomTabsIntent.Builder().build().launchUrl(this, Uri.parse(intent.getStringExtra(EXTRA_URL)))
val uri = Uri.parse(intent.getStringExtra(EXTRA_URL))
val tab = CustomTabsIntent.Builder().build()
tab.intent.data = uri
keepOutOfThisApp(tab.intent)
tab.launchUrl(this, uri)
} catch (e: ActivityNotFoundException) {
Log.e("Browser.OpenAuth", "No browser to open the auth URL", e)
AuthTab.closed(this)
Expand Down
52 changes: 48 additions & 4 deletions resources/android/BrowserFunctions.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.nativephp.browser

import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.util.Log
import androidx.browser.customtabs.CustomTabsIntent
Expand Down Expand Up @@ -31,7 +33,7 @@ object BrowserFunctions {
return try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
activity.startActivity(intent)
activity.startActivity(activity.keepOutOfThisApp(intent))

Log.d("Browser.Open", "Successfully opened URL in system browser")
mapOf("success" to true)
Expand All @@ -57,9 +59,12 @@ object BrowserFunctions {
Log.d("Browser.OpenInApp", "Opening URL in in-app browser: $url")

return try {
val uri = Uri.parse(url)
val builder = CustomTabsIntent.Builder()
val customTabsIntent = builder.build()
customTabsIntent.launchUrl(activity, Uri.parse(url))
customTabsIntent.intent.data = uri
activity.keepOutOfThisApp(customTabsIntent.intent)
customTabsIntent.launchUrl(activity, uri)

Log.d("Browser.OpenInApp", "Successfully opened URL in in-app browser")
mapOf("success" to true)
Expand All @@ -68,7 +73,7 @@ object BrowserFunctions {
// Fallback to system browser if Custom Tabs fail
return try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
activity.startActivity(intent)
activity.startActivity(activity.keepOutOfThisApp(intent))
Log.d("Browser.OpenInApp", "Opened URL in system browser (fallback)")
mapOf("success" to true)
} catch (fallbackError: Exception) {
Expand Down Expand Up @@ -105,4 +110,43 @@ object BrowserFunctions {
}
}
}
}
}

/**
* Make sure [intent], a VIEW intent for a URL, opens in a browser if Android would otherwise
* hand it back to this app.
*
* For a link the app holds an App Link for, a plain VIEW intent resolves straight to the app's
* own MainActivity, so the browser never opens and the link arrives as a deep link instead.
* Before Android 12 an unverified link can also put the app in the chooser next to the browsers.
*
* In those cases the intent gets a selector: the same request for the scheme alone, with no
* host. Android picks the activity from the selector and still delivers the real URL, and only a
* browser handles a web URL with no host, since an app's link filter always names one. So the
* user's default browser opens the URL (or, with no default set, the system asks which
* browser). Every other URL is left alone, so links to other apps (YouTube, Maps) still open in
* those apps.
*/
@Suppress("DEPRECATION")
internal fun Context.keepOutOfThisApp(intent: Intent): Intent {
val scheme = intent.data?.scheme?.lowercase()

if (scheme != "http" && scheme != "https") {
return intent
}

// The app's own package is always visible to it, so no <queries> entry is needed.
val candidates = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)

if (candidates.none { it.activityInfo.packageName == packageName }) {
return intent
}

Log.d("Browser", "${intent.data} would open in this app, sending it to a browser instead")

return intent.apply {
addCategory(Intent.CATEGORY_BROWSABLE)
selector = Intent(Intent.ACTION_VIEW, Uri.parse("$scheme://"))
.addCategory(Intent.CATEGORY_BROWSABLE)
}
}
13 changes: 13 additions & 0 deletions tests/PluginTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,19 @@
$parts = explode('.', $manifest['android']['init_function']);
expect($kotlinContent)->toContain('fun '.end($parts).'(context: Context)');
});

it('sends the app\'s own links to a browser on Android instead of back into the app', function () {
$functions = file_get_contents($this->pluginPath.'/resources/android/BrowserFunctions.kt');
$authTab = file_get_contents($this->pluginPath.'/resources/android/AuthTab.kt');

expect($functions)->toContain('internal fun Context.keepOutOfThisApp(intent: Intent): Intent');
expect($functions)->toContain('selector = Intent(Intent.ACTION_VIEW, Uri.parse("$scheme://"))');

// Open, OpenInApp (and its plain fallback) and OpenAuth's tab all launch through it.
expect($functions)->toContain('activity.startActivity(activity.keepOutOfThisApp(intent))');
expect($functions)->toContain('activity.keepOutOfThisApp(customTabsIntent.intent)');
expect($authTab)->toContain('keepOutOfThisApp(tab.intent)');
});
});

describe('Composer Configuration', function () {
Expand Down
Loading