Skip to content

Commit

Permalink
Merge pull request #28 from hotwired/location-access
Browse files Browse the repository at this point in the history
Location access via JavaScript
  • Loading branch information
jayohms authored Dec 2, 2024
2 parents a103ddc + 8946989 commit 83a61c5
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
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
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)) {
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
}
}

private fun manifestPermissions(): Array<String> {
return try {
val context = session.context
val packageInfo = context.packageManager.getPackageInfo(
context.packageName,
PackageManager.GET_PERMISSIONS
)

packageInfo.requestedPermissions
} catch (e: PackageManager.NameNotFoundException) {
logError("manifestPermissionsNotAvailable", e)
emptyArray()
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ import androidx.activity.result.ActivityResultLauncher
interface VisitDestination {
fun isActive(): Boolean
fun activityResultLauncher(requestCode: Int): ActivityResultLauncher<Intent>?
fun activityPermissionResultLauncher(requestCode: Int): ActivityResultLauncher<String>?
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)

Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,19 +160,35 @@ 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.
*/
fun activityResultLauncher(requestCode: Int): ActivityResultLauncher<Intent>? {
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<String>? {
return null
}

fun prepareNavigation(onReady: () -> Unit)

override fun bridgeWebViewIsReady(): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -63,6 +64,13 @@ open class HotwireWebBottomSheetFragment : HotwireBottomSheetFragment(), Hotwire
}
}

override fun activityPermissionResultLauncher(requestCode: Int): ActivityResultLauncher<String>? {
return when (requestCode) {
HOTWIRE_REQUEST_CODE_GEOLOCATION_PERMISSION -> webDelegate.geoLocationPermissionResultLauncher
else -> null
}
}

override fun onStart() {
super.onStart()
webDelegate.onStart()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -100,6 +101,13 @@ open class HotwireWebFragment : HotwireFragment(), HotwireWebFragmentCallback {
}
}

override fun activityPermissionResultLauncher(requestCode: Int): ActivityResultLauncher<String>? {
return when (requestCode) {
HOTWIRE_REQUEST_CODE_GEOLOCATION_PERMISSION -> webDelegate.geoLocationPermissionResultLauncher
else -> null
}
}

final override fun prepareNavigation(onReady: () -> Unit) {
webDelegate.prepareNavigation(onReady)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -163,6 +169,10 @@ internal class HotwireWebFragmentDelegate(
return navDestination.activityResultLauncher(requestCode)
}

override fun activityPermissionResultLauncher(requestCode: Int): ActivityResultLauncher<String>? {
return navDestination.activityPermissionResultLauncher(requestCode)
}

// -----------------------------------------------------------------------
// SessionCallback interface
// -----------------------------------------------------------------------
Expand Down Expand Up @@ -343,6 +353,12 @@ internal class HotwireWebFragmentDelegate(
}
}

private fun registerGeolocationPermissionLauncher(): ActivityResultLauncher<String> {
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 {
Expand Down

0 comments on commit 83a61c5

Please sign in to comment.