Let's say I have a component that needs to be initialized and destroyed depending on the lifecycle of the activity. However, this component needs to be granted permissions from the user first. What is the best way to do that?
Do I have to subscribe to the same observer in two different positions or there is a better way to do it without code duplication?
You can implement life cycle aware class encapsulates permission sensitive work:
class MyLifecycleAware {
private var blObject: Any? = null
/**
* Manually call this method when permission granted
*/
#OnLifecycleEvent(Lifecycle.Event.ON_START)
fun init() = withPermission {
// code will be invoked only if permission was granted
blObject = TODO("Initialize business logic")
}
#OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun destroy() {
blObject?.destroy()
blObject = null
}
/**
* Wrap any permission sensitive actions with this check
*/
private inline fun withPermission(action: () -> Unit) {
val permissionGranted = TODO("Check permission granted")
if (permissionGranted)
action()
}
}
Related
When I have my Location Provider as a Singleton, and assume I don't have the location permission and my condition closes the flow.
Then calling highAccuracyLocationFlow() function won't create callbackFlow again.
If I remove the #singleton, it works but creates multiple instances of the class for each subscriber.
How can I go about it?
#Singleton
class DefaultLocationProvider #Inject constructor(
#ApplicationContext private val context: Context,
private val fusedLocationClient: FusedLocationProviderClient,
) : LocationProvider {
init {
Timber.d("init: ")
}
private val _receivingLocationUpdates: MutableStateFlow<Boolean> =
MutableStateFlow(false)
override val receivingLocationUpdates: StateFlow<Boolean>
get() = _receivingLocationUpdates
private var _lastKnownLocation : Location? = null
override fun getLastKnownLocation(): Location? {
return _lastKnownLocation
}
private lateinit var locationRequest: LocationRequest
private val highAccuracyLocationRequest = LocationRequest.create().apply {
interval = TimeUnit.SECONDS.toMillis(2)
fastestInterval = TimeUnit.SECONDS.toMillis(1)
priority = Priority.PRIORITY_HIGH_ACCURACY
smallestDisplacement = 0f
}
private val balancedPowerLocationRequest = LocationRequest.create().apply {
interval = TimeUnit.SECONDS.toMillis(60)
fastestInterval = TimeUnit.SECONDS.toMillis(30)
priority = Priority.PRIORITY_BALANCED_POWER_ACCURACY
smallestDisplacement = 50f
}
#SuppressLint("MissingPermission")
private val _locationUpdates = callbackFlow {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
Timber.d("New location: ${result.lastLocation.toString()}")
// Send the new location to the Flow observers
_lastKnownLocation = result.lastLocation
result.lastLocation?.let {
trySend(it).isSuccess
}
}
}
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION)
!= PackageManager.PERMISSION_GRANTED
) close()
Timber.d("Starting location updates")
_receivingLocationUpdates.value = true
fusedLocationClient.requestLocationUpdates(
locationRequest,
callback,
Looper.getMainLooper()
).addOnFailureListener { e ->
e.printStackTrace()
close(e) // in case of exception, close the Flow
}
awaitClose {
Timber.d("Stopping location updates")
_receivingLocationUpdates.value = false
fusedLocationClient.removeLocationUpdates(callback) // clean up when Flow collection ends
}
}.shareIn(
MainScope(),
replay = 1,
started = SharingStarted.WhileSubscribed()
)
override fun highAccuracyLocationFlow(): Flow<Location> {
Timber.d("highAccuracyLocationFlow req")
locationRequest = highAccuracyLocationRequest
return _locationUpdates
}
override fun balancedPowerLocationFlow(): Flow<Location> {
locationRequest = balancedPowerLocationRequest
return _locationUpdates
}
}
Side note, I see a bug in your code. You used _locationUpdates with shareIn so there can only be one session of requestLocationUpdates going on at a time. So if you call balancedPowerLocationFlow() and start collecting that flow, followed by calling highAccuracyLocationFlow() while there is still a subscriber of the balanced power flow, it will remain as a balanced power flow even for the new subscriber.
Here are a couple of different strategies for the issue you're asking about:
Mark your two functions with #RequiresPermission(ACCESS_FINE_LOCATION) so you are leaving it up to the caller to only get the Flow reference if it knows the permission is already granted. The annotation helps catch some situations where you accidentally forget to check for the permission first. If you do this, you can remove the safety check that closes the Flow.
Create an internal Channel for tracking when the permission has been granted. Any class that uses this class can be responsible for informing it when permission has been granted (or that permission has already been granted).
private val permissionGrantedChannel = Channel<Unit>()
fun notifyLocationPermissionGranted() {
permissionGrantedChannel.trySend(Unit)
}
Then you can replace your if(/*...*/) close() with permissionGrantedChannel.receive() so the Flow simply suspends until it is known that the permissions have been granted.
Edit: Actually, this should probably be a MutableStateFlow instead of channel so when your flow gets restarted due to falling to 0 subscribers momentarily, the true value is already there. I put this version in the code below.
Here is a potential strategy for the issue I mentioned at the top. I didn't test this. The idea here is that we keep track in a StateFlow of how many high-accuracy subscriptions are currently being collected and use flatMapLatest on that to automatically restart our flow with the right type of location updates whenever we move between 0 and 1 collectors that require high accuracy.
A high-accuracy flow is wrapped at the start and end with making updates to that StateFlow, but otherwise is just passing through the same flow as you would get if requesting balanced updates. So there is only ever one location request going on at once. Collectors that only want balanced updates will simply get temporarily swapped to high accuracy whenever there is at least one high accuracy collector simultaneously getting updates.
Note, this is just to illustrate the concept. I removed a lot of your boilerplate just for brevity.
private val scope = MainScope() + CoroutineName("DefaultLocationProvider CoroutineScope")
private var highAccuracyCollectorCount = 0
private val isHighAccuracy = MutableStateFlow(false)
private val permissionGranted = MutableStateFlow(false)
fun notifyLocationPermissionGranted() {
permissionGranted.value = true
}
#OptIn(ExperimentalCoroutinesApi::class)
val balancedPowerLocations = isHighAccuracy.flatMapLatest { shouldUseHighAccuracy ->
callbackFlow {
val callback = object : LocationCallback() {
//...
}
permissionGranted.first { it }
fusedLocationClient.requestLocationUpdates(
locationRequest,
callback,
Looper.getMainLooper()
).addOnFailureListener { e ->
e.printStackTrace()
close(e) // in case of exception, close the Flow
}
awaitClose {
fusedLocationClient.removeLocationUpdates(callback)
}
}
}
.distinctUntilChanged() // in case switching callbacks tries to replay the last known location
.shareIn(scope, SharingStarted.WhileSubscribed(), replay = 1)
val highAccuracyLocations = balancedPowerLocations
.onStart {
isHighAccuracy.value = true
highAccuracyCollectorCount++
}
.onCompletion {
isHighAccuracy.value = --highAccuracyCollectorCount > 0
}
I am struggling with figure out how to close a dialog launched to explain denied permissions.
Using accompanist to ask for permissions:
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(key1 = lifecycleOwner, effect = {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
locationPermissionState.launchPermissionRequest()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
})
In the same composable I launch a dialog depending on denied permissions:
when {
locationPermissionState.status.shouldShowRationale -> {
AlertDialog(
// Dialog to explain to users the permission
)
}
!locationPermissionState.status.isGranted && !locationPermissionState.status.shouldShowRationale -> {
AlertDialog(
// dialog to tell user they need to go to settings to enable
)
}
}
I am stuck figuring out how to close on the dialog when the user click an OK button.
I have tried to use another state that survives recomposition:
val openDialog by remember { mutableStateOf(false) }
....
// if permission state denied
openDialog = true
....
// then in dialog ok
openDialog = false
However when doing that and changing the state of openDialog the function is recomposed. Which just means when I check the permissions state again its still the same and my dialog opens again.
For a general solution handling location permissions requests in Compose you need to keep track of performed permissions requests. Android's permissions request system works in an iterative fashion, and at certain points in this iteration there is no state change to observe and to act upon besides the iteration count. The current Accompanist 0.24.12-rc has a permissions request callback that you can use to do this. Then you can structure your declarative Compose code to take action based on previously observed and saved iteration counts and the current iteration count. (And you could try to abstract it further, creating dedicated states based on iteration count values and differences, but that's not really necessary to make it work; my hope would be that someone will add this in Accompanist at some point.)
For example:
val locationPermissions: List<String> = listOf(ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION)
#Stable
interface LocationPermissionsState : MultiplePermissionsState {
/**
* Supplies a well-defined measure of time/progression
*/
val requestCount: UInt
}
#Composable
fun rememberLocationPermissionsState(): LocationPermissionsState {
val requestCountState: MutableState<UInt> = remember { mutableStateOf(0u) }
val multiplePermissionsState: MultiplePermissionsState =
rememberMultiplePermissionsState(locationPermissions) {
if (it.isEmpty()) {
// BUG in accompanist-permissions library upon configuration change
return#rememberMultiplePermissionsState
}
requestCountState.value++
}
return object : LocationPermissionsState, MultiplePermissionsState by multiplePermissionsState {
override val requestCount: UInt by requestCountState
}
}
You can see this solution with a little more context at https://github.com/google/accompanist/issues/819
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.
Currently, I am writing a chat application for Android. Starting from 23 SDK and above, it needs some permissions which user has to allow, such as extreme important (my chat will use a location of creation of a particular chat) and just small features such as uploading images to Firebase storage (it needs the access to phone storage, therefore it needs appropriate permission).
I have the following interface for callbacks.
object PermissionUtils {
interface PermissionAskListener {
fun onPermissionGranted()
/*
User has already granted this permission
The app must had been launched earlier and the user must had "allowed" that permission
*/
fun onPermissionRequest()
/*
The app is launched FIRST TIME..
We don't need to show additional dialog, we just request for the permission..
*/
fun onPermissionPreviouslyDenied()
/*
The app was launched earlier and the user simply "denied" the permission..
The user had NOT clicked "DO NOT SHOW AGAIN"
We need to show additional dialog in this case explaining how "allowing this permission" would be useful to the user
*/
fun onPermissionDisabled()
/*
The app had launched earlier and the user "denied" the permission..
AND ALSO had clicked "DO NOT ASK AGAIN"
We need to show Toask/alertdialog/.. to indicate that the user had denied the permission by checking do not disturb too...
So, you might want to take the user to setting>app>permission page where the user can allow the permission..
*/
}
fun checkForPermission(activity: Activity, permission: String, permissionAskListener: PermissionAskListener) {
//code omitted, here's the logic of calls listener members
}
}
And, I use it in code like this:
//calling from onCreate()
checkForPermission(
this, android.Manifest.permission.READ_EXTERNAL_STORAGE,
object : PermissionAskListener {
override fun onPermissionGranted() {
showToast(getString(R.string.msg_permissions_granted), Toast.LENGTH_LONG)
uplodAnImageToFirebase()
}
override fun onPermissionRequest() {
ActivityCompat.requestPermissions(
this#MainActivity, arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE), readStorage
)
}
override fun onPermissionPreviouslyDenied() {
AlertDialog.Builder(this#MainActivity)
.setTitle(getString(R.string.title_permission_required))
.setMessage(getString(R.string.msg_permission_required))
.setCancelable(false)
.setPositiveButton(getString(R.string.action_allow)) { _, _ ->
ActivityCompat.requestPermissions(
this#MainActivity,
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
readStorage
)
}
.setNegativeButton(getString(R.string.action_cancel)) { dialog, _ ->
dialog.cancel()
showToast(getString(R.string.msg_we_cant_give_functionality), Toast.LENGTH_LONG)
}
.show()
}
override fun onPermissionDisabled() {
AlertDialog.Builder(this#MainActivity)
.setTitle(getString(R.string.title_permission_disabled))
.setMessage(getString(R.string.msg_please_enable_permission))
.setCancelable(false)
.setPositiveButton(
getString(R.string.action_go_to_settings)
) { _, _ -> startActivity(Intent(Settings.ACTION_SETTINGS)) }
.setNegativeButton(getString(R.string.action_cancel)) { dialog, _ ->
dialog.cancel()
showToast(getString(R.string.msg_we_cant_give_functionality), Toast.LENGTH_LONG)
}
.show()
}
}
)
As you may see from code, only onPermissionGranted() do something particular, and either onPermissionPreviouslyDenied() and onPermissionDisabled() just saying to user boring and common things, which I want to incapsulate to some class that will create either dialogs for extra important things (like location; if permission denied I'd like to close entire app), and either just upload, which will just block functionality.
I know how to do such requests for permission and other stuff like it. I don't know how to create these classes with the dialogs - create enum that I pass whenever I call onDisabled/onPreviouslyDenied method from activity, or create Builder for it, or create Factory... if you TL;DR case, then just answer: 'How to reduce the same code in my case?'
An alternative solution would be to create a BaseActivity class, and have your other activities in the app sub-class the BaseActivity.
Something like..
class BaseActivity: AppCompatActivity {
override fun onCreate() {
super.onCreate()
checkForPermissions() // do your permission check code
}
}
class MainActivity: BaseActivity {
override fun onCreate() {
super.onCreate() // calls BaseAcivitiy's onCreate, which triggers the checkForPermissions
}
}
As CommonsWare suggested in the comments, there is (here is libraries to try to reduce some of this boilerplate) a lot of good libraries. I've chosen a NoPermission library. Maybe, it seems like an advertising, but I'm sure that this is not the worst library. Have a nice day!!!
I suggest use the following library:
https://github.com/Karumi/Dexter
On the other hand, to avoid duplicate code, you can move to separate class the code related to dialog helper.
For instance: https://github.com/jpgpuyo/MVPvsMVVM/blob/one_dialog/app/src/main/java/upday/mvpvsmvvm/dialoghelper/DialogHelper.java
I could not find any information, if it's a bad idea to use LiveData without a lifecycle owner. And if it is, what could be the alternative?
Let me give you just a simple example
class Item() {
private lateinit var property: MutableLiveData<Boolean>
init {
property.value = false
}
fun getProperty(): LiveData<Boolean> = property
fun toggleProperty() {
property.value = when (property.value) {
false -> true
else -> false
}
}
}
class ItemHolder {
private val item = Item()
private lateinit var observer: Observer<Boolean>
fun init() {
observer = Observer<Boolean> { item ->
updateView(item)
}
item.getProperty().observeForever(observer)
}
fun destroy() {
item.getProperty().removeObserver(observer)
}
fun clickOnButton() {
item.toggleProperty();
}
private fun updateView(item: Boolean?) {
// do something
}
}
You can register an observer without an associated LifecycleOwner object using the
observeForever(Observer) method
like that:
orderRepo.getServices().observeForever(new Observer<List<Order>>() {
#Override
public void onChanged(List<Order> orders) {
//
}
});
You can register an observer without an associated LifecycleOwner object using the observeForever(Observer) method. In this case, the observer is considered to be always active and is therefore always notified about modifications. You can remove these observers calling the removeObserver(Observer) method.
Ref
https://developer.android.com/topic/libraries/architecture/livedata.html#work_livedata
For me LiveData has two benefits:
It aware of life cycle events and will deliver updates only in an appropriate state of a subscriber (Activity/Fragment).
It holds the last posted value, and updates with it new subscribers.
As already been said, if you're using it out of the life cycle components (Activity/Fragment) and the delivered update could be managed anytime, then you can use it without life cycle holder, otherwise, sooner or later, it may result in a crash, or data loss.
As an alternative to the LiveData behavior, I can suggest a BehaviourSubject from RxJava2 framework, which acts almost the same, holding the last updated value, and updating with it new subscribers.