I've made a LocationManager to handle permission issues and return lastLocation from FusedLocationProviderClient. It uses RxJava heavily to avoid callback hell. Here's the code:
object LocationManager {
// using coarse location to not ask for GPS enabling
// after changing to ACCESS_FINE_LOCATION don't forget to check GPS settings
private const val LOCATION_PERMISSION = Manifest.permission.ACCESS_COARSE_LOCATION
private var permissionSubject = AsyncSubject.create<Boolean>()
fun loadLastLocation(activity: Activity): Observable<Location?> =
checkPermission(activity).flatMap { hasPermission ->
// check permission, then request last location
Log.d("qwerty", "hasPermission=$hasPermission")
if (hasPermission) requestLastLocation(activity)
else Observable.error<Location>(Exception("Permission not granted"))
}
private fun checkPermission(activity: Activity): Observable<Boolean> =
if (activity.hasPermission(LOCATION_PERMISSION)) Observable.just(true)
else requestPermission(activity)
private fun requestPermission(activity: Activity): Observable<Boolean> = permissionSubject.apply {
// result will be posted to subject later
Log.d("qwerty", "requestPermission $LOCATION_PERMISSION")
ActivityCompat.requestPermissions(activity, arrayOf(LOCATION_PERMISSION), RequestCode.LOCATION_PERMISSION)
}
// call this from hosting activity or you can never get lastLocation
fun onRequestPermissionsResult(requestCode: Int, grantResults: IntArray) {
if (requestCode == RequestCode.LOCATION_PERMISSION) {
permissionSubject.apply {
val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
Log.d("qwerty", "onRequestPermissionsResult=$granted")
onNext(granted)
onComplete()
}
}
// skip other request codes
}
#SuppressLint("MissingPermission")
// check ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION before calling this
private fun requestLastLocation(activity: Activity): Observable<Location?> = PublishSubject.create<Location>().apply {
Log.d("qwerty", "requestLastLocation")
LocationServices.getFusedLocationProviderClient(activity).lastLocation
.addOnSuccessListener { location ->
Log.d("qwerty", "lastLocation=$location")
onNext(location)
onComplete()
}
.addOnFailureListener { error ->
Log.d("qwerty", "lastLocation error: ${error.message}")
onError(error)
}
}
}
And fragment calls LocationManager like this:
LocationManager.loadLastLocation(activity!!)
.subscribe(
{ location -> Log.d("qwerty", "fragment got location $location")},
{ error -> error.printStackTrace() }
)
The problem is Observable stucks in infinite loop trying to get permission. Here's what I get in logs, infinite times:
qwerty: requestPermission android.permission.ACCESS_COARSE_LOCATION
qwerty: hasPermission=false
System.err: java.lang.Exception: Permission not granted...
qwerty: onRequestPermissionsResult=false
Can someone tell me what's wrong with this code?
as far as I can see from this code, it should work properly. Please check in your manifest that it tag contain the same permission that you are requesting. And second one, please check in you Location manager imports, that proper Manifest class is imported (android.Manifest not your.app.package.Manifest).
Related
In Android 13, I need a basic flow to get permission for push notifications:
class MainActivity : ComponentActivity(), LocationListener {
val notificationPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted) {
// Permission is granted. Continue the action or workflow in your
// app.
} else {
// Explain to the user that the feature is unavailable because the
// feature requires a permission that the user has denied. At the
// same time, respect the user's decision. Don't link to system
// settings in an effort to convince the user to change their
// decision.
}
}
private fun requestPushNotificationPermissions(){
if ((ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED)) {
// granted
}else {
// not granted, ask for permission
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
}
This is what happened:
when user first installed the app, checkSelfPermission returns not granted, and we then lauch permission launcher
user sees the permission dialog in Android 13
user selects Allow
Expected: registerForActivityResult callback will be fired with isGranted true
Actual: registerForActivityResult callback is not fired.
Same if user selects Not Allow. callback is never fired. Why?
This is my dependencies:
implementation 'androidx.activity:activity-ktx:1.2.0-alpha07'
implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha07'
sadly can't help why it doesn't work. But I used EasyPermission for handling the permissons request and it works fine.
https://github.com/googlesamples/easypermissions
Turns out, the registerForActivityResult callback never fires, because somewhere in the Activity, there is this piece of old function "onRequestPermissionsResult" that is accidentally catching all permissions callback:
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (requestCode == locationPermissionCode) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//Location permission granted
}
else {
//Location permission denied
}
}else{
//Notification permission callback accidentally landed here silently
}
}
Hope this helps someone.
In Android 11, when user select "deny" option for more than once for any permission request, then system will mark it as "Permanently denied".Once permanently denied, user has to enable in settings.From this time shouldShowRequestPermissionRationale() start's to return false
Three options are available for permission window , "Deny","Allow All time","Allow only this time". But in settings "Deny","Allow all the time","Ask every time" are present.
How to find when user selects "Ask me every time" from settings, because, checkSelfPermission() returns PackageManager.PERMISSION_DENIED,and shouldShowRequestPermissionRationale() return false. In this time I want to show permission window, instead of move to settings. Something similar to google map permission
Using the new ActivityResultsContract you can do this in the following manner
private val requestPermissionLauncher =
registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { result: MutableMap<String, Boolean> ->
val deniedList: List<String> = result.filter {
!it.value
}.map {
it.key
}
when {
deniedList.isNotEmpty() -> {
val map = deniedList.groupBy { permission ->
if (shouldShowRequestPermissionRationale(permission)) DENIED else EXPLAINED
}
map[DENIED]?.let {
// request denied , request again
}
map[EXPLAINED]?.let {
//request denied ,send to settings
}
}
else -> {
//All request are permitted
}
}
}
In OnCreate()[Make sure you ask permission in OnCreate , else application will crash] , ask the permission :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestPermissionLauncher.launch(REQUIRED_PERMISSIONS)
}
Ask requiredPermissions in the following manner :
private val REQUIRED_PERMISSIONS = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.READ_EXTERNAL_STORAGE
)
Actually there is one more scenario:
if the user let you request the permission but don't chose any option and dismissed the dialog by tapping outside, the request finishes with denied and shouldShowRequestPermissionRationale() returns false.
That is the exact same behaviour as if the user selects don't ask again.
the permission where requested once, it results in denied and we should not show a explanation.
therefore we have to track if shouldShowRequestPermissionRationale() has returned true for once. if it switches back to false its denied permanent.
Use the following method in your activity (Camera permission is used in this example):
private fun requestPermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
//Permission is denied
} else {
//ask permission
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), REQUEST_CODE_CAMERA)
}
}
}
You only need to check the shouldShowRequestPermissionRationale() after user deny the permission
val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
} else {
if(shouldShowRequestPermissionRationale(Manifest.permission.YOUR_RUNTIME_PERMISSION)){
//user hasn't denied permission permanently
}else{
//user has denied permanently,
//you should try to redirect user to settings
//to ask user grant it manually
}
}
}
requestPermissionLauncher.launch(Manifest.permission.YOUR_RUNTIME_PERMISSION)
I have simple fragment that aim to fetch user current location.
The error I got is clear, we need to check if the user grant us location permissions:
Call requires permission which may be rejected by user: code should
explicitly check to see if permission is available (with
checkPermission) or explicitly handle a potential SecurityException
But I already made permission check, basically the fetch couldn't be done without user granting us permission, so why do I need to check twice?
My flow is like this:
if (activityListener.checkPermissions()) {
showMapAndFetchLocation()
}
Inside showMapAndFetchLocation method
initLocationProvider()
val mapFragment =
childFragmentManager.findFragmentById(R.id.map) as SupportMapFragment?
mapFragment?.getMapAsync(callback)
And last, inside initLocationProvider method
if (isLocationEnabled()) {
fusedLocationClient //**Error**
.lastLocation //**Error**
.addOnCompleteListener { task: Task<Location> ->
val location = task.result
location?.let {
Log.d(TAG, "getUserLastLocation: LAT ${it.latitude}")
Log.d(TAG, "getUserLastLocation: LNG ${it.longitude}")
}
?: requestNewLocationData()
}
}
As you can see, I'm checking frist if (activityListener.checkPermissions()) and only then fire the fetching process.
Do I really need to check twice or am I doing something wrong?
As #Pawel Suggested, I needed to add try/catch block and catch SecurityException exception.
Now the method looks like this:
try {
fusedLocationClient
.lastLocation
.addOnCompleteListener { task: Task<Location> ->
val location = task.result
location?.let {
Log.d(TAG, "getUserLastLocation: LAT ${it.latitude}")
Log.d(TAG, "getUserLastLocation: LNG ${it.longitude}")
setLngLat(it.latitude, it.longitude)
initMap()
}
?: requestNewLocationData()
}
} catch (e: SecurityException) {
activityListener.isLocationPermissionGranted()
}
I'm making an Android App that adds a map to a activity, the user can center the map with their current location using the "location layer". to add the location button in the map with:
mMap.isMyLocationEnabled = true
I'm checking for the ACCESS_FINE_LOCATION permission inside onMapReady method and everything works fine, but I need finish the activiy and re-open it to see the changes (the location button).
So, I'm using onRequestPermissionsResult to check the user response, but when I call mMap.isMyLocationEnabled = true Android Studio says:
Call requires permisson which may be rejected by user: code shoud
explicitly check to see if permission is available (with
checkPermission) or explicitly handle a potencial SecurityException...
This is my code:
override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap
mMap.uiSettings.isCompassEnabled = true
if(ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(this, arrayOf(android.Manifest.permission.ACCESS_FINE_LOCATION),SOLICITA_UBICACION_CLAVE)
}else{
mMap.isMyLocationEnabled = true
}
mMap.uiSettings.isMyLocationButtonEnabled = true
mMap.uiSettings.isZoomControlsEnabled = true
// Add a marker in Sydney and move the camera
val sydney = LatLng(-34.0, 151.0)
mMap.addMarker(MarkerOptions().position(sydney).title("Marker in Sydney"))
mMap.moveCamera(CameraUpdateFactory.newLatLng(sydney))
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId){
android.R.id.home -> {
onBackPressed()
return true
}
}
return super.onOptionsItemSelected(item)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
when(requestCode){
SOLICITA_UBICACION_CLAVE -> {
if((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)){
mMap.isMyLocationEnabled = true
Toast.makeText(this#MapsActivity,"Permiso garantizado",Toast.LENGTH_SHORT).show()
}else{
mMap.isMyLocationEnabled = false
Toast.makeText(this#MapsActivity,"Permiso denegado",Toast.LENGTH_SHORT).show()
}
}
}
}
So, How can I call mMap.isMyLocationEnabled = true inside onRequestPermissionsResult?,
What's the purpose to check a permission inside when instruction for the request code if is supposed that is currently granted?
So, How can I call mMap.isMyLocationEnabled = true inside onRequestPermissionsResult?,
Do it the way that you are, and add the appropriate #SuppressLint annotation to suppress the warning. There should be a quick-fix for this in Android Studio that will add#SuppressLint with the right property.
Presumably, there is some bug or limitation in the Lint rule that is causing your problem. For example, this is a relatively old Lint check, and so perhaps it is not handling all Kotlin scenarios correctly.
I am quite new in android programming. I would like to ask about startActivityForResult() and ActivityCompat.requestPermissions() function and their design. I understand that result of those functions is handled by another Activity functions (onActivityResult() and onRequestPermissionsResult() respectively). But I don't understand why is it designed this way.
Especially with ActivityCompat.requestPermissions(). Why do I have to control if I have permission (ContextCompat.checkSelfPermission()), if I don't then ask for it (ActivityCompat.requestPermissions()). And then handle in completely different function if I got this permission or not?
I would expect somethink like:
askPermission(Context context, String permission, Runnable permissionGranted, Runnable permissionDenied)
which would call permissionGranted if I already have permission or if I got it from user. With this function I would have to care just if I have permission or I don't have it.
Now I have to distunguish if I have permission and then do synchronous task or I don't have it and then do "asynchronous" task in onRequestPermissionsResult() where I very often do the same, as I do if I already have permission.
My question is: Is there some reason, why are permissions designed this way? Is there some funtion as I wrote above to allow me just say what to do if I have and what to do if i don't have permission (in functional way)? Or is there some desing pattern to easy handle permissions and starting activities for result?
Thanks for your time and some explanation if you know why is this design good.
Definitely Not a good way!
If we use inheritence concept we may solve this problem a little
we can make it synchronous like this :
//Kotlin
askForPermissions(permissionList, onPermissionsGranted = {
//If permissions given
}, onPermissionFailed = {
//If permissions not given
})
buy using inheritence :
//Kotlin
open class PermissionActivity : AppCompatActivity() {
private val PERM_REQ_CODE = 1457
private lateinit var onPermissionsGranted: () -> Unit;
private lateinit var onPermissionFailed: () -> Unit;
private lateinit var perms: Array<String>
internal fun askForPermissions(perms: Array<String>, onPermissionsGranted: () -> Unit, onPermissionFailed: () -> Unit) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (checkIfOneNotHasPermission(perms)) {
//Dont have permissions
this.perms = perms
this.onPermissionsGranted = onPermissionsGranted
this.onPermissionFailed = onPermissionFailed
requestPermissions(perms, PERM_REQ_CODE)
}
} else {
onPermissionsGranted.invoke()
}
}
#RequiresApi(Build.VERSION_CODES.M)
private fun checkIfOneNotHasPermission(perms: Array<String>): Boolean {
perms.forEach {
if (checkSelfPermission(it) != PackageManager.PERMISSION_GRANTED) {
return true
}
}
return false
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
when (requestCode) {
PERM_REQ_CODE -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (checkIfOneNotHasPermission(perms)) {
onPermissionFailed.invoke()
} else {
onPermissionsGranted.invoke()
}
} else {
onPermissionsGranted.invoke()
}
}
else -> {
onPermissionFailed.invoke()
}
}
}
}