From 30f66e9a4cf59440f9741944193bd6d2dd0aaefe Mon Sep 17 00:00:00 2001 From: Gary Mathews Date: Tue, 31 Aug 2021 09:29:54 -0700 Subject: [PATCH] fix(android): request ACCESS_COARSE_LOCATION permission (#12884) * fix(android): request ACCESS_COARSE_LOCATION permission * chore(android): improve coarse location permission handling * fix(android): update getLocationServicesEnabled() * fix(android): handle absence of ACCESS_FINE_LOCATION on Android 12 * fix(android): validate location permissions * fix(android): onLocationChanged() null check Co-authored-by: Joshua Quick Co-authored-by: Ewan Harris Co-authored-by: Lokesh Choudhary --- .../geolocation/GeolocationModule.java | 107 +++++++++++++++--- .../titanium/geolocation/TiLocation.java | 23 ---- .../ui/widget/webview/TiWebChromeClient.java | 63 ++++++----- 3 files changed, 131 insertions(+), 62 deletions(-) diff --git a/android/modules/geolocation/src/java/ti/modules/titanium/geolocation/GeolocationModule.java b/android/modules/geolocation/src/java/ti/modules/titanium/geolocation/GeolocationModule.java index 2d47282e21b..2359d765fc9 100644 --- a/android/modules/geolocation/src/java/ti/modules/titanium/geolocation/GeolocationModule.java +++ b/android/modules/geolocation/src/java/ti/modules/titanium/geolocation/GeolocationModule.java @@ -42,11 +42,15 @@ import android.location.Address; import android.location.Geocoder; import android.location.Location; +import android.location.LocationManager; import android.location.LocationProvider; import android.os.Build; import android.os.Handler; import android.os.Message; +import androidx.annotation.NonNull; +import androidx.core.location.LocationManagerCompat; + /** * GeolocationModule exposes all common methods and properties relating to geolocation behavior * associated with Ti.Geolocation to the Titanium developer. Only cross platform API points should @@ -144,6 +148,7 @@ public class GeolocationModule extends KrollModule implements Handler.Callback, private FusedLocationProvider fusedLocationProvider; private Geocoder geocoder; + private LocationManager locationManager; /** * Constructor @@ -156,6 +161,7 @@ public GeolocationModule() fusedLocationProvider = new FusedLocationProvider(context, this); geocoder = new Geocoder(context); + locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); tiLocation = new TiLocation(); tiCompass = new TiCompass(this, tiLocation); @@ -206,6 +212,9 @@ public boolean handleMessage(Message message) */ public void onLocationChanged(Location location) { + if (location == null) { + return; + } lastLocation = location; // Execute getCurrentPosition() callbacks/Promises @@ -497,12 +506,11 @@ public boolean hasLocationPermissions() if (Build.VERSION.SDK_INT < 23) { return true; } - Context context = TiApplication.getInstance().getApplicationContext(); - if (context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) - == PackageManager.PERMISSION_GRANTED) { - return true; - } - return false; + + Context context = TiApplication.getInstance(); + int result = context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION); + result &= context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION); + return (result == PackageManager.PERMISSION_GRANTED); } @SuppressLint("NewApi") @@ -512,6 +520,7 @@ public KrollPromise requestLocationPermissions(@Kroll.argument(option { final KrollObject callbackThisObject = getKrollObject(); return KrollPromise.create((promise) -> { + // Fetch the optional callback argument. KrollFunction permissionCB; if (type instanceof KrollFunction && permissionCallback == null) { permissionCB = (KrollFunction) type; @@ -519,7 +528,7 @@ public KrollPromise requestLocationPermissions(@Kroll.argument(option permissionCB = permissionCallback; } - // already have permissions, fall through + // Do not continue if we already have permission. if (hasLocationPermissions()) { KrollDict response = new KrollDict(); response.putCodeAndMessage(0, null); @@ -530,11 +539,72 @@ public KrollPromise requestLocationPermissions(@Kroll.argument(option return; } - TiBaseActivity.registerPermissionRequestCallback(TiC.PERMISSION_CODE_LOCATION, permissionCB, - callbackThisObject, promise); - Activity currentActivity = TiApplication.getInstance().getCurrentActivity(); - currentActivity.requestPermissions(new String[] { Manifest.permission.ACCESS_FINE_LOCATION }, - TiC.PERMISSION_CODE_LOCATION); + // Do not continue if there is no activity to host the request dialog. + Activity activity = TiApplication.getInstance().getCurrentActivity(); + if (activity == null) { + KrollDict response = + buildLocationErrorEvent(-1, "There are no activities to host the location request dialog."); + if (permissionCB != null) { + permissionCB.callAsync(callbackThisObject, response); + } + promise.reject(new Throwable(response.getString(TiC.EVENT_PROPERTY_ERROR))); + return; + } + + // Set up a custom callback to handle the user's grant/denial of this permission. + TiBaseActivity.OnRequestPermissionsResultCallback activityCallback; + activityCallback = new TiBaseActivity.OnRequestPermissionsResultCallback() { + @Override + public void onRequestPermissionsResult( + @NonNull TiBaseActivity activity, int requestCode, + @NonNull String[] permissions, @NonNull int[] grantResults) + { + // Unregister this callback. + TiBaseActivity.unregisterPermissionRequestCallback(TiC.PERMISSION_CODE_LOCATION); + + // Do not continue if there is no callback to invoke with the result. + if ((permissionCB) == null && (promise == null)) { + return; + } + + // Check if at least 1 location permission has been granted. + // Note: As of Android 12, COARSE permission can be granted while FINE is denied. + boolean wasGranted = false; + if (permissions.length == grantResults.length) { + for (int index = 0; index < permissions.length; index++) { + switch (permissions[index]) { + case Manifest.permission.ACCESS_COARSE_LOCATION: + case Manifest.permission.ACCESS_FINE_LOCATION: + wasGranted = (grantResults[index] == PackageManager.PERMISSION_GRANTED); + break; + } + if (wasGranted) { + break; + } + } + } + + // Invoke callback(s) with the result. + KrollDict response = new KrollDict(); + if (wasGranted) { + response.putCodeAndMessage(0, null); + promise.resolve(response); + } else { + response.putCodeAndMessage(-1, "Location permission denied."); + promise.reject(response); + } + if (permissionCB != null) { + permissionCB.callAsync(callbackThisObject, response); + } + } + }; + + // Prompt end-user for permission. + // Note: As of Android 12, we cannot request FINE permission by itself. We must also include COARSE. + TiBaseActivity.registerPermissionRequestCallback(TiC.PERMISSION_CODE_LOCATION, activityCallback); + activity.requestPermissions( + new String[] { Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION }, + TiC.PERMISSION_CODE_LOCATION); }); } @@ -628,6 +698,17 @@ private void doEnableLocationProviders(HashMap lo LocationProviderProxy locationProvider = locationProviders.get(iterator.next()); registerLocationProvider(locationProvider); } + + // On Android 12+, check for ACCESS_FINE_LOCATION. + // If ACCESS_FINE_LOCATION is denied, return last known location. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && hasLocationPermissions()) { + Context context = TiApplication.getInstance(); + int result = context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION); + + if (result == PackageManager.PERMISSION_DENIED) { + onLocationChanged(tiLocation.getLastKnownLocation()); + } + } } } @@ -660,7 +741,7 @@ private void disableLocationProviders() @Kroll.getProperty public boolean getLocationServicesEnabled() { - return tiLocation.getLocationServicesEnabled(); + return LocationManagerCompat.isLocationEnabled(locationManager); } /** diff --git a/android/modules/geolocation/src/java/ti/modules/titanium/geolocation/TiLocation.java b/android/modules/geolocation/src/java/ti/modules/titanium/geolocation/TiLocation.java index b1dcfe6e574..a6687f152ec 100644 --- a/android/modules/geolocation/src/java/ti/modules/titanium/geolocation/TiLocation.java +++ b/android/modules/geolocation/src/java/ti/modules/titanium/geolocation/TiLocation.java @@ -54,29 +54,6 @@ public boolean isProvider(String name) return knownProviders.contains(name); } - public boolean getLocationServicesEnabled() - { - // Fetch all enabled location providers. - List providerNames = locationManager.getProviders(true); - if ((providerNames == null) || (providerNames.size() <= 0)) { - return false; - } - - // Log all providers currently enabled. - if (Log.isDebugModeEnabled()) { - Log.i(TAG, "Enabled location provider count: " + providerNames.size()); - for (String providerName : providerNames) { - Log.i(TAG, providerName + " service available"); - } - } - - // Only return true if location can be obtained via GPS or WiFi/Cellular. - // Ignore "passive" provider and "test" providers. - boolean isEnabled = providerNames.contains(LocationManager.GPS_PROVIDER); - isEnabled |= providerNames.contains(LocationManager.NETWORK_PROVIDER); - return isEnabled; - } - @SuppressLint("MissingPermission") public Location getLastKnownLocation() { diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/webview/TiWebChromeClient.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/webview/TiWebChromeClient.java index 2c609d1b99c..4dfb479e411 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/webview/TiWebChromeClient.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/webview/TiWebChromeClient.java @@ -82,38 +82,49 @@ public void onGeolocationPermissionsShowPrompt( return; } - // Prompt end-user for permission on Android 6.0 and higher if needed. + // Prompt end-user for FINE location permission on Android 6.0 and higher if needed. + // Note: As of Android 12, we must also request the COARSE permission (will fail without it), + // but ignore COARSE permission since WebView location access requires FINE. if (Build.VERSION.SDK_INT >= 23) { int permissionResult = activity.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION); if (permissionResult != PackageManager.PERMISSION_GRANTED) { - TiBaseActivity.OnRequestPermissionsResultCallback activityCallback; - activityCallback = new TiBaseActivity.OnRequestPermissionsResultCallback() { - @Override - public void onRequestPermissionsResult( - @NonNull TiBaseActivity activity, int requestCode, - @NonNull String[] permissions, @NonNull int[] grantResults) - { - // Unregister this callback. - TiBaseActivity.unregisterPermissionRequestCallback(TiC.PERMISSION_CODE_LOCATION); - - // Determine if location permission was granted. - boolean wasGranted = false; - if (permissions.length == grantResults.length) { - for (int index = 0; index < permissions.length; index++) { - if (Manifest.permission.ACCESS_FINE_LOCATION.equals(permissions[index])) { - wasGranted = (grantResults[index] == PackageManager.PERMISSION_GRANTED); - break; + if (activity.shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)) { + // System won't prompt for permission since user already denied it. So, immediately fail it. + callback.invoke(origin, false, false); + } else { + // Prompt end-user for permission. + TiBaseActivity.OnRequestPermissionsResultCallback activityCallback; + activityCallback = new TiBaseActivity.OnRequestPermissionsResultCallback() { + @Override + public void onRequestPermissionsResult( + @NonNull TiBaseActivity activity, int requestCode, + @NonNull String[] permissions, @NonNull int[] grantResults) + { + // Unregister this callback. + TiBaseActivity.unregisterPermissionRequestCallback(TiC.PERMISSION_CODE_LOCATION); + + // Determine if FINE location permission was granted. (Ignore COARSE permission.) + boolean granted = false; + if (permissions.length == grantResults.length) { + for (int index = 0; index < permissions.length; index++) { + if (Manifest.permission.ACCESS_FINE_LOCATION.equals(permissions[index])) { + granted = (grantResults[index] == PackageManager.PERMISSION_GRANTED); + break; + } } } - } - // Notify WebView whether or not location access was granted. - callback.invoke(origin, wasGranted, false); - } - }; - TiBaseActivity.registerPermissionRequestCallback(TiC.PERMISSION_CODE_LOCATION, activityCallback); - String[] permissions = new String[] { Manifest.permission.ACCESS_FINE_LOCATION }; - activity.requestPermissions(permissions, TiC.PERMISSION_CODE_LOCATION); + // Notify WebView whether or not location access was granted. + callback.invoke(origin, granted, false); + } + }; + TiBaseActivity.registerPermissionRequestCallback(TiC.PERMISSION_CODE_LOCATION, activityCallback); + String[] permissions = new String[] { + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + }; + activity.requestPermissions(permissions, TiC.PERMISSION_CODE_LOCATION); + } return; } }