From 1eae77f06e20464f2632542b057513cd16b525cd Mon Sep 17 00:00:00 2001 From: Joe Masilotti Date: Fri, 5 Apr 2024 11:13:54 -0700 Subject: [PATCH 1/8] Location access via JavaScript --- demo/src/main/AndroidManifest.xml | 2 ++ .../hotwire/demo/features/web/WebFragment.kt | 33 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 23bfb08..fbad58b 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -3,6 +3,8 @@ xmlns:android="http://schemas.android.com/apk/res/android"> + + + if (isGranted) refresh() + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupMenu() @@ -35,6 +47,27 @@ open class WebFragment : HotwireWebFragment() { } } + override fun createWebChromeClient(): TurboWebChromeClient { + return object : TurboWebChromeClient(session) { + override fun onGeolocationPermissionsShowPrompt( + origin: String?, + callback: GeolocationPermissions.Callback? + ) { + if (ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED + ) { + permissionLauncher.launch( + Manifest.permission.ACCESS_FINE_LOCATION + ) + } else { + callback?.invoke(origin, true, false) + } + } + } + } + private fun setupMenu() { toolbarForNavigation()?.inflateMenu(R.menu.web) } From 0f81b707412e99aac08ce84f12aca36712d05a09 Mon Sep 17 00:00:00 2001 From: Jay Ohms Date: Sat, 23 Nov 2024 08:24:12 -0500 Subject: [PATCH 2/8] Automatically support Geolocation permission requests directly within the HotwireWebChromeClient if the app has declared location permission(s) are declared in the app manifest. --- .../GeolocationPermissionDelegate.kt | 102 ++++++++++++++++++ .../hotwire/core/files/util/FileConstants.kt | 4 + .../dev/hotwire/core/turbo/session/Session.kt | 9 ++ .../core/turbo/visit/VisitDestination.kt | 1 + .../turbo/webview/HotwireWebChromeClient.kt | 29 ++++- .../hotwire/demo/features/web/WebFragment.kt | 33 ------ .../destinations/HotwireDestination.kt | 22 +++- .../HotwireWebBottomSheetFragment.kt | 8 ++ .../fragments/HotwireWebFragment.kt | 10 +- .../fragments/HotwireWebFragmentDelegate.kt | 18 +++- 10 files changed, 195 insertions(+), 41 deletions(-) create mode 100644 core/src/main/kotlin/dev/hotwire/core/files/delegates/GeolocationPermissionDelegate.kt diff --git a/core/src/main/kotlin/dev/hotwire/core/files/delegates/GeolocationPermissionDelegate.kt b/core/src/main/kotlin/dev/hotwire/core/files/delegates/GeolocationPermissionDelegate.kt new file mode 100644 index 0000000..24d4a6a --- /dev/null +++ b/core/src/main/kotlin/dev/hotwire/core/files/delegates/GeolocationPermissionDelegate.kt @@ -0,0 +1,102 @@ +package dev.hotwire.core.files.delegates + +import android.Manifest.permission.ACCESS_COARSE_LOCATION +import android.Manifest.permission.ACCESS_FINE_LOCATION +import android.content.Context +import android.content.pm.PackageManager +import android.webkit.GeolocationPermissions +import androidx.core.content.ContextCompat +import androidx.core.content.PermissionChecker +import dev.hotwire.core.files.util.HOTWIRE_REQUEST_CODE_GEOLOCATION_PERMISSION +import dev.hotwire.core.logging.logError +import dev.hotwire.core.turbo.session.Session + +class GeolocationPermissionDelegate( + private val session: Session +) { + private val context: Context = session.context + private val permissionToRequest = preferredLocationPermission() + + private var requestOrigin: String? = null + private var requestCallback: GeolocationPermissions.Callback? = null + + fun onRequestPermission( + origin: String?, + callback: GeolocationPermissions.Callback? + ) { + requestOrigin = origin + requestCallback = callback + + if (requestOrigin == null || requestCallback == null || permissionToRequest == null) { + permissionDenied() + } else if (hasLocationPermission(context)) { + permissionGranted() + } else { + startPermissionRequest() + } + } + + fun onActivityResult(isGranted: Boolean) { + if (isGranted) { + permissionGranted() + } else { + permissionDenied() + } + } + + private fun startPermissionRequest() { + val destination = session.currentVisit?.callback?.visitDestination() ?: return + val resultLauncher = destination.activityPermissionResultLauncher(HOTWIRE_REQUEST_CODE_GEOLOCATION_PERMISSION) + + try { + resultLauncher?.launch(permissionToRequest) + } catch (e: Exception) { + logError("startGeolocationPermissionError", e) + permissionDenied() + } + } + + private fun hasLocationPermission(context: Context): Boolean { + return permissionToRequest?.let { + ContextCompat.checkSelfPermission(context, it) == PermissionChecker.PERMISSION_GRANTED + } == true + } + + private fun permissionGranted() { + requestCallback?.invoke(requestOrigin, true, true) + requestOrigin = null + requestCallback = null + } + + private fun permissionDenied() { + requestCallback?.invoke(requestOrigin, false, false) + requestOrigin = null + requestCallback = null + } + + private fun preferredLocationPermission(): String? { + val declaredPermissions = manifestPermissions().filter { + it == ACCESS_COARSE_LOCATION || + it == ACCESS_FINE_LOCATION + } + + // Prefer fine location if provided in manifest, otherwise coarse location + return if (declaredPermissions.contains(ACCESS_FINE_LOCATION)) { + ACCESS_FINE_LOCATION + } else if (declaredPermissions.contains(ACCESS_COARSE_LOCATION)) { + ACCESS_COARSE_LOCATION + } else { + null + } + } + + private fun manifestPermissions(): Array { + val context = session.context + val packageInfo = context.packageManager.getPackageInfo( + context.packageName, + PackageManager.GET_PERMISSIONS + ) + + return packageInfo.requestedPermissions + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/dev/hotwire/core/files/util/FileConstants.kt b/core/src/main/kotlin/dev/hotwire/core/files/util/FileConstants.kt index ef06fab..1a69c4b 100644 --- a/core/src/main/kotlin/dev/hotwire/core/files/util/FileConstants.kt +++ b/core/src/main/kotlin/dev/hotwire/core/files/util/FileConstants.kt @@ -1,3 +1,7 @@ package dev.hotwire.core.files.util +// Intent activity launcher request codes const val HOTWIRE_REQUEST_CODE_FILES = 37 + +// Permission activity launcher request codes +const val HOTWIRE_REQUEST_CODE_GEOLOCATION_PERMISSION = 3737 diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/session/Session.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/session/Session.kt index c194668..449e361 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/session/Session.kt +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/session/Session.kt @@ -16,6 +16,7 @@ import androidx.webkit.WebViewFeature.isFeatureSupported import dev.hotwire.core.config.Hotwire import dev.hotwire.core.logging.logEvent import dev.hotwire.core.files.delegates.FileChooserDelegate +import dev.hotwire.core.files.delegates.GeolocationPermissionDelegate import dev.hotwire.core.turbo.errors.HttpError import dev.hotwire.core.turbo.errors.LoadError import dev.hotwire.core.turbo.errors.WebError @@ -76,8 +77,16 @@ class Session( var isRenderProcessGone = false internal set + /** + * The delegate that handles WebView-requested file chooser requests. + */ val fileChooserDelegate = FileChooserDelegate(this) + /** + * The delegate the handles WebView-requested geolocation permission requests. + */ + val geolocationPermissionDelegate = GeolocationPermissionDelegate(this) + init { initializeWebView() HotwireHttpClient.enableCachingWith(context) diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/visit/VisitDestination.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/visit/VisitDestination.kt index ad17525..823a5f4 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/visit/VisitDestination.kt +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/visit/VisitDestination.kt @@ -6,4 +6,5 @@ import androidx.activity.result.ActivityResultLauncher interface VisitDestination { fun isActive(): Boolean fun activityResultLauncher(requestCode: Int): ActivityResultLauncher? + fun activityPermissionResultLauncher(requestCode: Int): ActivityResultLauncher? } diff --git a/core/src/main/kotlin/dev/hotwire/core/turbo/webview/HotwireWebChromeClient.kt b/core/src/main/kotlin/dev/hotwire/core/turbo/webview/HotwireWebChromeClient.kt index 62e0444..f6e3402 100644 --- a/core/src/main/kotlin/dev/hotwire/core/turbo/webview/HotwireWebChromeClient.kt +++ b/core/src/main/kotlin/dev/hotwire/core/turbo/webview/HotwireWebChromeClient.kt @@ -2,6 +2,7 @@ package dev.hotwire.core.turbo.webview import android.net.Uri import android.os.Message +import android.webkit.GeolocationPermissions import android.webkit.JsResult import android.webkit.ValueCallback import android.webkit.WebChromeClient @@ -13,7 +14,12 @@ import dev.hotwire.core.turbo.util.toJson import dev.hotwire.core.turbo.visit.VisitOptions open class HotwireWebChromeClient(val session: Session) : WebChromeClient() { - override fun onJsAlert(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean { + override fun onJsAlert( + view: WebView?, + url: String?, + message: String?, + result: JsResult? + ): Boolean { val context = view?.context ?: return false MaterialAlertDialogBuilder(context) @@ -29,7 +35,12 @@ open class HotwireWebChromeClient(val session: Session) : WebChromeClient() { return true } - override fun onJsConfirm(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean { + override fun onJsConfirm( + view: WebView?, + url: String?, + message: String?, + result: JsResult? + ): Boolean { val context = view?.context ?: return false MaterialAlertDialogBuilder(context) @@ -60,7 +71,12 @@ open class HotwireWebChromeClient(val session: Session) : WebChromeClient() { ) } - override fun onCreateWindow(webView: WebView, isDialog: Boolean, isUserGesture: Boolean, resultMsg: Message?): Boolean { + override fun onCreateWindow( + webView: WebView, + isDialog: Boolean, + isUserGesture: Boolean, + resultMsg: Message? + ): Boolean { val message = webView.handler.obtainMessage() webView.requestFocusNodeHref(message) @@ -73,4 +89,11 @@ open class HotwireWebChromeClient(val session: Session) : WebChromeClient() { return false } + + override fun onGeolocationPermissionsShowPrompt( + origin: String?, + callback: GeolocationPermissions.Callback? + ) { + session.geolocationPermissionDelegate.onRequestPermission(origin, callback) + } } diff --git a/demo/src/main/kotlin/dev/hotwire/demo/features/web/WebFragment.kt b/demo/src/main/kotlin/dev/hotwire/demo/features/web/WebFragment.kt index a7fca36..c272df5 100644 --- a/demo/src/main/kotlin/dev/hotwire/demo/features/web/WebFragment.kt +++ b/demo/src/main/kotlin/dev/hotwire/demo/features/web/WebFragment.kt @@ -1,18 +1,12 @@ package dev.hotwire.demo.features.web -import android.Manifest -import android.content.pm.PackageManager import android.os.Bundle import android.view.MenuItem import android.view.View -import android.webkit.GeolocationPermissions -import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.content.ContextCompat import dev.hotwire.core.turbo.errors.HttpError import dev.hotwire.core.turbo.errors.VisitError import dev.hotwire.core.turbo.visit.VisitAction.REPLACE import dev.hotwire.core.turbo.visit.VisitOptions -import dev.hotwire.core.turbo.webview.HotwireWebChromeClient import dev.hotwire.demo.R import dev.hotwire.demo.Urls import dev.hotwire.navigation.destinations.HotwireDestinationDeepLink @@ -20,12 +14,6 @@ import dev.hotwire.navigation.fragments.HotwireWebFragment @HotwireDestinationDeepLink(uri = "hotwire://fragment/web") open class WebFragment : HotwireWebFragment() { - private val permissionLauncher = registerForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted -> - if (isGranted) refresh() - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupMenu() @@ -47,27 +35,6 @@ open class WebFragment : HotwireWebFragment() { } } - override fun createWebChromeClient(): HotwireWebChromeClient { - return object : HotwireWebChromeClient(navigator.session) { - override fun onGeolocationPermissionsShowPrompt( - origin: String?, - callback: GeolocationPermissions.Callback? - ) { - if (ContextCompat.checkSelfPermission( - requireContext(), - Manifest.permission.ACCESS_FINE_LOCATION - ) != PackageManager.PERMISSION_GRANTED - ) { - permissionLauncher.launch( - Manifest.permission.ACCESS_FINE_LOCATION - ) - } else { - callback?.invoke(origin, true, false) - } - } - } - } - private fun setupMenu() { toolbarForNavigation()?.inflateMenu(R.menu.web) } diff --git a/navigation-fragments/src/main/java/dev/hotwire/navigation/destinations/HotwireDestination.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/destinations/HotwireDestination.kt index ff593bf..ae31dc8 100644 --- a/navigation-fragments/src/main/java/dev/hotwire/navigation/destinations/HotwireDestination.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/destinations/HotwireDestination.kt @@ -159,12 +159,13 @@ interface HotwireDestination : BridgeDestination { } /** - * Gets a registered activity result launcher instance for the given `requestCode`. + * Gets a registered `ActivityResultContracts.StartActivityForResult` activity result launcher + * instance for the given `requestCode`. * * Override to provide your own [androidx.activity.result.ActivityResultLauncher] * instances. If your app doesn't have a matching `requestCode`, you must call - * `super.activityResultLauncher(requestCode)` to give the Turbo library an - * opportunity to provide a matching result launcher. + * `super.activityResultLauncher(requestCode)` to give the library an opportunity + * to provide a matching result launcher. * * @param requestCode The request code for the corresponding result launcher. */ @@ -172,6 +173,21 @@ interface HotwireDestination : BridgeDestination { return null } + /** + * Gets a registered `ActivityResultContracts.RequestPermission` activity result launcher + * instance for the given `requestCode`. + * + * Override to provide your own [androidx.activity.result.ActivityResultLauncher] + * instances. If your app doesn't have a matching `requestCode`, you must call + * `super.activityPermissionResultLauncher(requestCode)` to give the library an + * opportunity to provide a matching result launcher. + * + * @param requestCode The request code for the corresponding result launcher. + */ + fun activityPermissionResultLauncher(requestCode: Int): ActivityResultLauncher? { + return null + } + fun prepareNavigation(onReady: () -> Unit) override fun bridgeWebViewIsReady(): Boolean { diff --git a/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireWebBottomSheetFragment.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireWebBottomSheetFragment.kt index e4e8f79..59766d0 100644 --- a/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireWebBottomSheetFragment.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireWebBottomSheetFragment.kt @@ -11,6 +11,7 @@ import androidx.activity.result.ActivityResultLauncher import dev.hotwire.core.bridge.BridgeDelegate import dev.hotwire.core.turbo.errors.VisitError import dev.hotwire.core.files.util.HOTWIRE_REQUEST_CODE_FILES +import dev.hotwire.core.files.util.HOTWIRE_REQUEST_CODE_GEOLOCATION_PERMISSION import dev.hotwire.core.turbo.webview.HotwireWebChromeClient import dev.hotwire.core.turbo.webview.HotwireWebView import dev.hotwire.navigation.R @@ -63,6 +64,13 @@ open class HotwireWebBottomSheetFragment : HotwireBottomSheetFragment(), Hotwire } } + override fun activityPermissionResultLauncher(requestCode: Int): ActivityResultLauncher? { + return when (requestCode) { + HOTWIRE_REQUEST_CODE_GEOLOCATION_PERMISSION -> webDelegate.geoLocationPermissionResultLauncher + else -> null + } + } + override fun onStart() { super.onStart() webDelegate.onStart() diff --git a/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireWebFragment.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireWebFragment.kt index 220fd16..b88a0e2 100644 --- a/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireWebFragment.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireWebFragment.kt @@ -8,8 +8,9 @@ import android.view.View import android.view.ViewGroup import androidx.activity.result.ActivityResultLauncher import dev.hotwire.core.bridge.BridgeDelegate -import dev.hotwire.core.turbo.errors.VisitError import dev.hotwire.core.files.util.HOTWIRE_REQUEST_CODE_FILES +import dev.hotwire.core.files.util.HOTWIRE_REQUEST_CODE_GEOLOCATION_PERMISSION +import dev.hotwire.core.turbo.errors.VisitError import dev.hotwire.core.turbo.webview.HotwireWebChromeClient import dev.hotwire.core.turbo.webview.HotwireWebView import dev.hotwire.navigation.R @@ -100,6 +101,13 @@ open class HotwireWebFragment : HotwireFragment(), HotwireWebFragmentCallback { } } + override fun activityPermissionResultLauncher(requestCode: Int): ActivityResultLauncher? { + return when (requestCode) { + HOTWIRE_REQUEST_CODE_GEOLOCATION_PERMISSION -> webDelegate.geoLocationPermissionResultLauncher + else -> null + } + } + final override fun prepareNavigation(onReady: () -> Unit) { webDelegate.prepareNavigation(onReady) } diff --git a/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireWebFragmentDelegate.kt b/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireWebFragmentDelegate.kt index 7a8a0a8..547a0ee 100644 --- a/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireWebFragmentDelegate.kt +++ b/navigation-fragments/src/main/java/dev/hotwire/navigation/fragments/HotwireWebFragmentDelegate.kt @@ -4,6 +4,7 @@ import android.content.Intent import android.graphics.Bitmap import android.webkit.HttpAuthHandler import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts.RequestPermission import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.lifecycle.Lifecycle.State.STARTED import androidx.lifecycle.findViewTreeLifecycleOwner @@ -13,11 +14,11 @@ import dev.hotwire.core.config.Hotwire import dev.hotwire.core.turbo.config.pullToRefreshEnabled import dev.hotwire.core.turbo.errors.VisitError import dev.hotwire.core.turbo.session.SessionCallback -import dev.hotwire.core.turbo.webview.HotwireWebView import dev.hotwire.core.turbo.visit.Visit import dev.hotwire.core.turbo.visit.VisitAction import dev.hotwire.core.turbo.visit.VisitDestination import dev.hotwire.core.turbo.visit.VisitOptions +import dev.hotwire.core.turbo.webview.HotwireWebView import dev.hotwire.navigation.destinations.HotwireDestination import dev.hotwire.navigation.session.SessionModalResult import dev.hotwire.navigation.util.dispatcherProvider @@ -61,6 +62,11 @@ internal class HotwireWebFragmentDelegate( */ val fileChooserResultLauncher = registerFileChooserLauncher() + /** + * The activity result launcher that handles geolocation permission results. + */ + val geoLocationPermissionResultLauncher = registerGeolocationPermissionLauncher() + fun prepareNavigation(onReady: () -> Unit) { session.removeCallback(this) detachWebView(onReady) @@ -163,6 +169,10 @@ internal class HotwireWebFragmentDelegate( return navDestination.activityResultLauncher(requestCode) } + override fun activityPermissionResultLauncher(requestCode: Int): ActivityResultLauncher? { + return navDestination.activityPermissionResultLauncher(requestCode) + } + // ----------------------------------------------------------------------- // SessionCallback interface // ----------------------------------------------------------------------- @@ -343,6 +353,12 @@ internal class HotwireWebFragmentDelegate( } } + private fun registerGeolocationPermissionLauncher(): ActivityResultLauncher { + return navDestination.fragment.registerForActivityResult(RequestPermission()) { isGranted -> + session.geolocationPermissionDelegate.onActivityResult(isGranted) + } + } + private fun visit(location: String, restoreWithCachedSnapshot: Boolean, reload: Boolean) { val restore = restoreWithCachedSnapshot && !reload val options = when { From ed9bf47fdfdf462a4d3662b14e045ae0bd7763bc Mon Sep 17 00:00:00 2001 From: Jay Ohms Date: Sat, 23 Nov 2024 10:37:57 -0500 Subject: [PATCH 3/8] Only request "fine" location permission since it's the only compatible permission with the WebView --- .../GeolocationPermissionDelegate.kt | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/core/src/main/kotlin/dev/hotwire/core/files/delegates/GeolocationPermissionDelegate.kt b/core/src/main/kotlin/dev/hotwire/core/files/delegates/GeolocationPermissionDelegate.kt index 24d4a6a..d136d36 100644 --- a/core/src/main/kotlin/dev/hotwire/core/files/delegates/GeolocationPermissionDelegate.kt +++ b/core/src/main/kotlin/dev/hotwire/core/files/delegates/GeolocationPermissionDelegate.kt @@ -1,6 +1,5 @@ package dev.hotwire.core.files.delegates -import android.Manifest.permission.ACCESS_COARSE_LOCATION import android.Manifest.permission.ACCESS_FINE_LOCATION import android.content.Context import android.content.pm.PackageManager @@ -11,11 +10,9 @@ import dev.hotwire.core.files.util.HOTWIRE_REQUEST_CODE_GEOLOCATION_PERMISSION import dev.hotwire.core.logging.logError import dev.hotwire.core.turbo.session.Session -class GeolocationPermissionDelegate( - private val session: Session -) { +class GeolocationPermissionDelegate(private val session: Session) { private val context: Context = session.context - private val permissionToRequest = preferredLocationPermission() + private val permissionToRequest = locationPermission() private var requestOrigin: String? = null private var requestCallback: GeolocationPermissions.Callback? = null @@ -46,7 +43,9 @@ class GeolocationPermissionDelegate( private fun startPermissionRequest() { val destination = session.currentVisit?.callback?.visitDestination() ?: return - val resultLauncher = destination.activityPermissionResultLauncher(HOTWIRE_REQUEST_CODE_GEOLOCATION_PERMISSION) + val resultLauncher = destination.activityPermissionResultLauncher( + HOTWIRE_REQUEST_CODE_GEOLOCATION_PERMISSION + ) try { resultLauncher?.launch(permissionToRequest) @@ -74,20 +73,11 @@ class GeolocationPermissionDelegate( requestCallback = null } - private fun preferredLocationPermission(): String? { - val declaredPermissions = manifestPermissions().filter { - it == ACCESS_COARSE_LOCATION || - it == ACCESS_FINE_LOCATION - } - - // Prefer fine location if provided in manifest, otherwise coarse location - return if (declaredPermissions.contains(ACCESS_FINE_LOCATION)) { - ACCESS_FINE_LOCATION - } else if (declaredPermissions.contains(ACCESS_COARSE_LOCATION)) { - ACCESS_COARSE_LOCATION - } else { - null - } + private fun locationPermission(): String? { + // Only request "fine" location if provided in the app manifest, since + // the WebView requires this permission for location access. Granting + // "coarse" location does not work. See: https://issues.chromium.org/issues/40205003 + return manifestPermissions().firstOrNull { it == ACCESS_FINE_LOCATION } } private fun manifestPermissions(): Array { From f7284854143265c9a63587634ffb7ac075742c0f Mon Sep 17 00:00:00 2001 From: Jay Ohms Date: Sun, 24 Nov 2024 23:32:19 -0500 Subject: [PATCH 4/8] Permit fine or coarse location permission requests, except for Android 12 --- .../GeolocationPermissionDelegate.kt | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/dev/hotwire/core/files/delegates/GeolocationPermissionDelegate.kt b/core/src/main/kotlin/dev/hotwire/core/files/delegates/GeolocationPermissionDelegate.kt index d136d36..5c8f440 100644 --- a/core/src/main/kotlin/dev/hotwire/core/files/delegates/GeolocationPermissionDelegate.kt +++ b/core/src/main/kotlin/dev/hotwire/core/files/delegates/GeolocationPermissionDelegate.kt @@ -1,8 +1,10 @@ package dev.hotwire.core.files.delegates +import android.Manifest.permission.ACCESS_COARSE_LOCATION import android.Manifest.permission.ACCESS_FINE_LOCATION import android.content.Context import android.content.pm.PackageManager +import android.os.Build import android.webkit.GeolocationPermissions import androidx.core.content.ContextCompat import androidx.core.content.PermissionChecker @@ -12,7 +14,7 @@ import dev.hotwire.core.turbo.session.Session class GeolocationPermissionDelegate(private val session: Session) { private val context: Context = session.context - private val permissionToRequest = locationPermission() + private val permissionToRequest = preferredLocationPermission() private var requestOrigin: String? = null private var requestCallback: GeolocationPermissions.Callback? = null @@ -73,7 +75,29 @@ class GeolocationPermissionDelegate(private val session: Session) { requestCallback = null } - private fun locationPermission(): String? { + private fun preferredLocationPermission(): String? { + val declaredPermissions = manifestPermissions().filter { + it == ACCESS_COARSE_LOCATION || + it == ACCESS_FINE_LOCATION + } + + // Prefer fine location if provided in manifest, otherwise coarse location + return if (declaredPermissions.contains(ACCESS_FINE_LOCATION)) { + ACCESS_FINE_LOCATION + } else if (declaredPermissions.contains(ACCESS_COARSE_LOCATION)) { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S || + Build.VERSION.SDK_INT == Build.VERSION_CODES.S_V2) { + // Android 12 requires the "fine" permission for location + // access within the WebView. Granting "coarse" location does not + // work. See: https://issues.chromium.org/issues/40205003 + null + } else { + ACCESS_COARSE_LOCATION + } + } else { + null + } + // Only request "fine" location if provided in the app manifest, since // the WebView requires this permission for location access. Granting // "coarse" location does not work. See: https://issues.chromium.org/issues/40205003 From 528af20b514058d4cb1295f48e8980539151b1df Mon Sep 17 00:00:00 2001 From: Jay Ohms Date: Sun, 24 Nov 2024 23:34:23 -0500 Subject: [PATCH 5/8] Fix test --- .../test/kotlin/dev/hotwire/core/turbo/session/SessionTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/src/test/kotlin/dev/hotwire/core/turbo/session/SessionTest.kt b/core/src/test/kotlin/dev/hotwire/core/turbo/session/SessionTest.kt index 6472f3f..07e79d9 100644 --- a/core/src/test/kotlin/dev/hotwire/core/turbo/session/SessionTest.kt +++ b/core/src/test/kotlin/dev/hotwire/core/turbo/session/SessionTest.kt @@ -1,6 +1,7 @@ package dev.hotwire.core.turbo.session import android.os.Build +import androidx.activity.result.ActivityResultLauncher import androidx.appcompat.app.AppCompatActivity import com.nhaarman.mockito_kotlin.never import com.nhaarman.mockito_kotlin.times @@ -54,6 +55,7 @@ class SessionTest { val visitDestination = object : VisitDestination { override fun isActive() = true override fun activityResultLauncher(requestCode: Int) = null + override fun activityPermissionResultLauncher(requestCode: Int) = null } whenever(callback.visitDestination()).thenReturn(visitDestination) From dbfa30e2264baf183fb232a70b69e717cd7496d0 Mon Sep 17 00:00:00 2001 From: Jay Ohms Date: Sun, 24 Nov 2024 23:47:38 -0500 Subject: [PATCH 6/8] Remove the gelocation permision declarations from the demo app --- demo/src/main/AndroidManifest.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index fbad58b..23bfb08 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -3,8 +3,6 @@ xmlns:android="http://schemas.android.com/apk/res/android"> - - Date: Mon, 2 Dec 2024 10:05:54 -0500 Subject: [PATCH 7/8] Remove unreachable code --- .../core/files/delegates/GeolocationPermissionDelegate.kt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/core/src/main/kotlin/dev/hotwire/core/files/delegates/GeolocationPermissionDelegate.kt b/core/src/main/kotlin/dev/hotwire/core/files/delegates/GeolocationPermissionDelegate.kt index 5c8f440..994f358 100644 --- a/core/src/main/kotlin/dev/hotwire/core/files/delegates/GeolocationPermissionDelegate.kt +++ b/core/src/main/kotlin/dev/hotwire/core/files/delegates/GeolocationPermissionDelegate.kt @@ -97,11 +97,6 @@ class GeolocationPermissionDelegate(private val session: Session) { } else { null } - - // Only request "fine" location if provided in the app manifest, since - // the WebView requires this permission for location access. Granting - // "coarse" location does not work. See: https://issues.chromium.org/issues/40205003 - return manifestPermissions().firstOrNull { it == ACCESS_FINE_LOCATION } } private fun manifestPermissions(): Array { From 8946989e15f28bd30a03c73d8c07853187be1c48 Mon Sep 17 00:00:00 2001 From: Jay Ohms Date: Mon, 2 Dec 2024 10:12:25 -0500 Subject: [PATCH 8/8] Catch manifest permission package exceptions --- .../GeolocationPermissionDelegate.kt | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/core/src/main/kotlin/dev/hotwire/core/files/delegates/GeolocationPermissionDelegate.kt b/core/src/main/kotlin/dev/hotwire/core/files/delegates/GeolocationPermissionDelegate.kt index 994f358..2bf8f75 100644 --- a/core/src/main/kotlin/dev/hotwire/core/files/delegates/GeolocationPermissionDelegate.kt +++ b/core/src/main/kotlin/dev/hotwire/core/files/delegates/GeolocationPermissionDelegate.kt @@ -100,12 +100,17 @@ class GeolocationPermissionDelegate(private val session: Session) { } private fun manifestPermissions(): Array { - val context = session.context - val packageInfo = context.packageManager.getPackageInfo( - context.packageName, - PackageManager.GET_PERMISSIONS - ) + return try { + val context = session.context + val packageInfo = context.packageManager.getPackageInfo( + context.packageName, + PackageManager.GET_PERMISSIONS + ) - return packageInfo.requestedPermissions + packageInfo.requestedPermissions + } catch (e: PackageManager.NameNotFoundException) { + logError("manifestPermissionsNotAvailable", e) + emptyArray() + } } -} \ No newline at end of file +}