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()
}
}
}
}
Related
I'm working on a library that has a couple of ready-made activities.
So far i have my activities in the library, and in the main app, i call it normally with registerForActivityResult to start it.
this means whoever is using my library would be able to see the whole activity.
what i would like to do, is to have the developer call a method in the library class and ask it to do an action, and in the library that method would on its own start the activity, register it for result, and return the result to the calling class through an interface.
the below is what i tried but it gives me error LifecycleOwner is attempting to register while current state is RESUMED. LifecycleOwners must call register before they are STARTED
private fun launchScannerActivity(activity: FragmentActivity, callback: ScannerCallback) {
val scanResult =
activity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
callback.onResult(it.data?.getStringExtra("Some Key") ?: "")
} else {
callback.onFail()
}
}
val intent = Intent(activity, ScannerActivity::class.java)
scanResult.launch(intent);
}
why do i need this:
This library would be an SDK for a SAAS product, so we would like to abstract and obfuscate as much of the implementation as possible from our clients.
You can't really communicate between Activities using interfaces, at least not in a way that is somewhat concise and isn't very prone to leaking. What you can do is expose your own Activity result contract. Then your API could be as simple as some of the ones in ActivityResultContracts. You can look at the source code there to see how to implement it.
Maybe something like this:
class ScannerResultContract : ActivityResultContract<Unit, String?>() {
override fun createIntent(context: Context, input: Unit?): Intent {
return Intent(context, ScannerActivity::class.java)
}
override fun parseResult(resultCode: Int, intent: Intent?): String? {
return if (resultCode == Activity.RESULT_OK) {
intent?.getStringExtra("Some Key")
} else {
null
}
}
}
Client usage:
// In activity or fragment:
val getScannerResult = registerForActivityResult(ScannerResultContract()) { resultString ->
if (resultString != null) {
// use it
} else {
// log no result returned
}
}
//elsewhere:
someListener.setOnClickListener {
getScannerResult.launch()
}
I have an activity that requires camera permission.
this activity can be called from several user configurable places in the app.
The rationale dialog and permission dialog themselves should be shown before the activity opens.
right now I am trying to handle these dialogs in some kind of extension function.
fun handlePermissions(context: Context, required_permissions: Array<String>, activity: FragmentActivity?, fragment: Fragment?): Boolean {
var isGranted = allPermissionsGranted(context, required_permissions)
if (!isGranted) {
//null here is where I used to pass my listener which was the calling fragment previously that implemented an interface
val dialog = DialogPermissionFragment(null, DialogPermissionFragment.PermissionType.QR)
activity?.supportFragmentManager?.let { dialog.show(it, "") }
//get result from dialog? how?
//if accepted launch actual permission request
fragment?.registerForActivityResult(ActivityResultContracts.RequestPermission()) { success ->
isGranted = success
}?.launch(android.Manifest.permission.CAMERA)
}
return isGranted
}
But I am having trouble to get the dialog results back from the rationale/explanation dialog.
My research until now brought me to maybe using a higher order function, to pass a function variable to the dialog fragment that returns a Boolean value to the helper function. But I am absolutely unsure if thats the right thing.
I thought using my own code would be cleaner and less overhead, could I achieve this easier when using a framework like eazy-permissions? (is Dexter still recommendable since its no longer under development)
is that even a viable thing I am trying to achieve here?
One approach that I've implemented and seems viable to use is this:
Class PermissionsHelper
class PermissionsHelper(private val activity: Activity) {
fun requestPermissionsFromDevice(
arrayPermissions: Array<String>, permissionsResultInterface: PermissionsResultInterface
) {
setPermissionResultInterface(permissionsResultInterface)
getMyPermissionRequestActivity().launch(arrayPermissions)
}
fun checkPermissionsFromDevice(permission: String): Boolean {
return ContextCompat.checkSelfPermission(activity, permission) ==
PackageManager.PERMISSION_GRANTED
}
}
Class PermissionsResultInterface to the helper class be able to communicate with the activity.
interface PermissionsResultInterface {
fun onPermissionFinishResult(permissions: MutableMap<String, Boolean>)
}
Usage with this approach to remove all files from app directory:
private fun clearFiles(firstCall: Boolean = false) {
if (verifyStoragePermissions(firstCall)) {
val dir = File(getExternalFilesDir(null).toString())
removeFileOrDirectory(dir)
Toast.makeText(
applicationContext,
resources.getString(R.string.done),
Toast.LENGTH_SHORT
).show()
}
}
private fun verifyStoragePermissions(firstCall: Boolean = false): Boolean {
val arrayListPermissions = arrayOf(
android.Manifest.permission.READ_EXTERNAL_STORAGE,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
)
for (permission in arrayListPermissions) {
if (!PermissionsHelper(this).checkPermissionsFromDevice(permission)) {
if (firstCall) PermissionsHelper(this)
.requestPermissionsFromDevice(arrayListPermissions, this)
else PermissionsDialogs(this).showPermissionsErrorDialog()
return false
}
}
return true
}
override fun onPermissionFinishResult(permissions: MutableMap<String, Boolean>) {
clearFiles()
}
With this approach you are able to call the permissions helper and using the result interface, after each of the answers from user, decide wether you still need to make a call for permissions or show a dialog to him.
If you need any help don't hesitate to contact me.
Is there a way to get current activity in compose function?
#Composable
fun CameraPreviewScreen() {
val context = ContextAmbient.current
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.CAMERA
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this, MainActivity.REQUIRED_PERMISSIONS, MainActivity.REQUEST_CODE_PERMISSIONS // get activity for `this`
)
return
}
}
While the previous answer (which is ContextWrapper-aware) is indeed the correct one,
I'd like to provide a more idiomatic implementation to copy-paste.
fun Context.getActivity(): AppCompatActivity? = when (this) {
is AppCompatActivity -> this
is ContextWrapper -> baseContext.getActivity()
else -> null
}
As ContextWrappers can't possibly wrap each other significant number of times, recursion is fine here.
You can get the activity from your composables casting the context (I haven't found a single case where the context wasn't the activity). However, has Jim mentioned, is not a good practice to do so.
val activity = LocalContext.current as Activity
Personally I use it when I'm just playing around some code that requires the activity (permissions is a good example) but once I've got it working, I simply move it to the activity and use parameters/callback.
Edit: As mentioned in the comments, using this in production code can be dangerous, as it can crash because current is a context wrapper, my suggestion is mostly for testing code.
To get the context
val context = LocalContext.current
Then get activity using the context. Create an extension function, and call this extension function with your context like context.getActivity().
fun Context.getActivity(): AppCompatActivity? {
var currentContext = this
while (currentContext is ContextWrapper) {
if (currentContext is AppCompatActivity) {
return currentContext
}
currentContext = currentContext.baseContext
}
return null
}
Rather than casting the Context to an Activity, you can safely use it by creating a LocalActivity.
val LocalActivity = staticCompositionLocalOf<ComponentActivity> {
noLocalProvidedFor("LocalActivity")
}
private fun noLocalProvidedFor(name: String): Nothing {
error("CompositionLocal $name not present")
}
Usage:
CompositionLocalProvider(LocalActivity provides this) {
val activity = LocalActivity.current
// your content
}
For requesting runtime permission in Jetpack Compose use Accompanist library: https://github.com/google/accompanist/tree/main/permissions
Usage example from docs:
#Composable
private fun FeatureThatRequiresCameraPermission(
navigateToSettingsScreen: () -> Unit
) {
// Track if the user doesn't want to see the rationale any more.
var doNotShowRationale by rememberSaveable { mutableStateOf(false) }
val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
PermissionRequired(
permissionState = cameraPermissionState,
permissionNotGrantedContent = {
if (doNotShowRationale) {
Text("Feature not available")
} else {
Column {
Text("The camera is important for this app. Please grant the permission.")
Spacer(modifier = Modifier.height(8.dp))
Row {
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("Ok!")
}
Spacer(Modifier.width(8.dp))
Button(onClick = { doNotShowRationale = true }) {
Text("Nope")
}
}
}
}
},
permissionNotAvailableContent = {
Column {
Text(
"Camera permission denied. See this FAQ with information about why we " +
"need this permission. Please, grant us access on the Settings screen."
)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = navigateToSettingsScreen) {
Text("Open Settings")
}
}
}
) {
Text("Camera permission Granted")
}
}
Also, if you check the source, you will find out, that Google uses same workaround as provided by Rajeev answer, so Jim's answer about bad practice is somewhat disputable.
This extention function allows you to specify activity you want to get:
inline fun <reified Activity : ComponentActivity> Context.getActivity(): Activity? {
return when (this) {
is Activity -> this
else -> {
var context = this
while (context is ContextWrapper) {
context = context.baseContext
if (context is Activity) return context
}
null
}
}
}
Example:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { HomeScreen() }
}
}
#Composable
fun HomeScreen() {
val activity = LocalContext.current.getActivity<MainActivity>()
}
Getting an activity from within a Composable function is considered a bad practice, as your composables should not be tightly coupled with the rest of your app. Among other things, a tight coupling will prevent you from unit-testing your composable and generally make reuse harder.
Looking at your code, it looks like you are requesting permissions from within the composable. Again, this is not something you want to be doing inside your composable, as composable functions can run as often as every frame, which means you would keep calling that function every frame.
Instead, setup your camera permissions in your activity, and pass down (via parameters) any information that is needed by your composable in order to render pixels.
Below is a slight modification to #Jeffset answer since Compose activities are based off of ComponentActivity and not AppCompatActivity.
fun Context.getActivity(): ComponentActivity? = when (this) {
is ComponentActivity -> this
is ContextWrapper -> baseContext.findActivity()
else -> null
}
I actually found this really cool extension function inside the accompanist
library to do this:
internal fun Context.findActivity(): Activity {
var context = this
while (context is ContextWrapper) {
if (context is Activity) return context
context = context.baseContext
}
throw IllegalStateException("Permissions should be called in the context of an Activity")
}
which gets used inside a composable function like this:
#Composable
fun composableFunc(){
val context = LocalContext.current
val activity = context.findActivity()
}
I followed this tutorial to implement a new approach to request application permissions via Results API by RequestMultiplePermissions contract. Although the permission dialog is shown and permission result is propagated through the system to application preferences etc., my provided ActivityResultCallback is not notified at all.
Here are my source codes. I am aware I am not checking whether the user hasn't declined the permission already:
private fun checkPermissions() {
val permissionList = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.ACCESS_FINE_LOCATION
)
val notGrantedPermissions = permissionList.map {
Pair(
it, ContextCompat.checkSelfPermission(
applicationContext,
it
)
)
}.filter {
it.second != PackageManager.PERMISSION_GRANTED
}
.map { it.first }
.toTypedArray()
if (notGrantedPermissions.isEmpty()) {
nextActivity()
} else {
requestPermissionLauncher.launch(notGrantedPermissions)
}
}
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { result ->
info("> requestPermissionLauncher - ${result.values}")
if (result.values.all { it }) {
nextActivity()
} else {
longToast("All permissions are required for app to work correctly")
checkPermissions()
}
}
Did I miss anything in the documentation?
Library version: androidx.activity:activity-ktx:1.2.0-alpha06
MinSdkVersion: 21
TargetSdkVersion: 29
I was up against this same issue tonight. I'm betting you're either extending AppCompatActivity or something similar. The issue with that is, when super.onRequestPermissionsResult is invoked, it falls to the FragmentActivity implementation which does not, itself, invoke the super method so the chain dies there. The quick solution is to extend ComponentActivity directly. However, if this is not feasible for your solution, you can override the method as follows:
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
activityResultRegistry.dispatchResult(requestCode, Activity.RESULT_OK, Intent()
.putExtra(EXTRA_PERMISSIONS, permissions)
.putExtra(EXTRA_PERMISSION_GRANT_RESULTS, grantResults))
}
The above is a direct port from the ComponentActivity method.
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).