tbruyelle / RxPermissions

Android runtime permissions powered by RxJava2

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Crash on rotation

Gi-lo opened this issue · comments

Hey there,

First of all nice work. Sadly I have to say that your solution currently does not work with rotation changes. Simply press "Enable Camera", rotate your device and press "DENY" on the popup. This will result in the following crash:

 "09-30 23:42:06.090  31948-31948/com.tbruyelle.rxpermissions.sample E/AndroidRuntime﹕ FATAL EXCEPTION: main
    Process: com.tbruyelle.rxpermissions.sample, PID: 31948
    java.lang.RuntimeException: Failure delivering result ResultInfo{who=@android:requestPermissions:, request=463403621, result=-1, data=Intent { act=android.content.pm.action.REQUEST_PERMISSIONS (has extras) }} to activity {com.tbruyelle.rxpermissions.sample/com.tbruyelle.rxpermissions.sample.MainActivity}: java.lang.IllegalStateException: RxPermission.onRequestPermissionsResult invoked but didn't find the corresponding permission request.
            at android.app.ActivityThread.deliverResults(ActivityThread.java:3699)
            at android.app.ActivityThread.handleSendResult(ActivityThread.java:3742)
            at android.app.ActivityThread.-wrap16(ActivityThread.java)
            at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1393)
            at android.os.Handler.dispatchMessage(Handler.java:102)
            at android.os.Looper.loop(Looper.java:148)
            at android.app.ActivityThread.main(ActivityThread.java:5417)
            at java.lang.reflect.Method.invoke(Native Method)
            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
     Caused by: java.lang.IllegalStateException: RxPermission.onRequestPermissionsResult invoked but didn't find the corresponding permission request.
            at com.tbruyelle.rxpermissions.RxPermissions.onRequestPermissionsResult(RxPermissions.java:123)
            at com.tbruyelle.rxpermissions.sample.MainActivity.onRequestPermissionsResult(MainActivity.java:36)
            at android.app.Activity.dispatchRequestPermissionsResult(Activity.java:6553)
            at android.app.Activity.dispatchActivityResult(Activity.java:6432)
            at android.app.ActivityThread.deliverResults(ActivityThread.java:3695)
            at android.app.ActivityThread.handleSendResult(ActivityThread.java:3742)
            at android.app.ActivityThread.-wrap16(ActivityThread.java)
            at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1393)
            at android.os.Handler.dispatchMessage(Handler.java:102)
            at android.os.Looper.loop(Looper.java:148)
            at android.app.ActivityThread.main(ActivityThread.java:5417)
            at java.lang.reflect.Method.invoke(Native Method)
            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)

Do you have any ideas how to tackle this problem?

Well it was something I expected, because I totally neglect lifecycle handling... until now :)
I will try to find a solution.

OK it will be hard to fix that problem properly.

Rxjava and activity/fragment lifecycle have been discussed a lot and as far as I know there is no clean solution.

We can easily prevent the crash by making RxPermissions a singleton, so after the rotation change and the user answers to the permission request, the corresponding subject is found. But when the lib emits the response, the observer is still linked to the old activity, so it has no effect to the current new activity.

The main problem here is we have rebind the subscription in the newly created activity, after the rotation change. This can be done manually by the user, he unsubscribes when the activity dies and resubscribe when the new activity pops. But I don't like that since it adds too much code for the user.

If someone has a good idea, it would be pleasure to hear.

I had a similar idea building such a library for onActivityResult()handling. Sadly I also stumbled across the rotation issue and never had the chance to really think about a suitable solution.

However:
I also cannot come up with a solution where you keep the current flow rx. request().subscribe().

Maybe @dlew , @passsy or @mttkay can share some input here. I think there once was a RxAndroid issue where configuration change has been discussed. However I am not able to find it anymore.

One way to handle is store observable outside the activity and resubscribe to it when needed. Since the original RxPermissions is bound to the consumer activity, I decided to try out using an invisible activity to handle all this. Seems to be working pretty well and rotation handling is possible when using RxPermissions singleton (or just cached somewhere) bound to an application context. PR coming soon ;]

as @tbruyelle said it can only be solved with a Singleton (, supporting only a single instance like @eleventigers did) or with a lot of code the user has to put into his activity. I solved such problems in the past by using a presenter (MVP) for my activity which gets passed to the new created Activity after a configuration change with onRetainNonConfigurationInstance(). Because I retain the presenter instance I can hold a reference to the RxPremissions instance there. Not sure if this solves this problem here. Somebody could try this with a simple Mosby MVP implementation.

This is just a workaround and no fix for this library.

IMO, if you want an Observable-based solution, there are two things you'll need to do:

  1. Persist request state in onSaveInstanceState() (in a Parcelable or similar) and restore it again in onCreate().
  2. Require re-subscription on recreation.

If you do the first step, then you can skip having to store the Observable altogether since you could just create a new one on recreation. It also gets rid of the need for a singleton.

As for why the second is necessary: The core problem is that you want a Subscription to outlast the Activity lifecycle, which just isn't going to happen. Even if you persist the Observable, the Subscription will necessarily need to be recreated along with the new Activity, since the old Subscriber will have no references to the new Activity.

(You could try to reattach the Subscriber to the new Activity but in what way is that better than simply recreating the Subscription?)


Of course this comes with all sorts of anti-fun changes to this lib, since users would have to implement onSaveInstanceState() and resubscription themselves.

FWIW, this is why I ended up not going the Observable route for my own permission handling code. I'm not saying this library isn't feasible, but there's a lot of plumbing necessary due to the lack of good Activity/Fragment lifecycle listeners.

FWIW, we found a very workable solution by using retained fragments and
ReplaySubjects. The key idea being that the retained fragment state
persists the observable across rotation changes, and the replay subject
makes sure that by unsubscribing/resubscribing throughout the rotation, the
subscriber won't miss out on any notifications.

I guess there's any number of other ways to solve that problem. I believe I
remember Jake saying that they solved it with Dagger singletons. They would
essentially keep sequences around in the object graph (which survives the
Activity), and a caching layer can ensure that you don't re-trigger
expensive network calls or the like.

I haven't yet heard of a "canonical" way of dealing with the problem.

On Thu, Oct 1, 2015 at 3:29 PM Daniel Lew notifications@github.com wrote:

IMO, if you want an Observable-based solution, there are two things
you'll need to do:

  1. Persist request state in onSaveInstanceState() (in a Parcelable or
    similar) and restore it again in onCreate().
  2. Require re-subscription on recreation.

If you do the first step, then you can skip having to store the Observable
altogether since you could just create a new one on recreation. It also
gets rid of the need for a singleton.

As for why the second is necessary: The core problem is that you want a
Subscription to outlast the Activity lifecycle, which just isn't going to
happen. Even if you persist the Observable, the Subscription will
necessarily need to be recreated along with the new Activity, since the
old Subscriber will have no references to the new Activity.

(You could try to reattach the Subscriber to the new Activity but in what
way is that better than simply recreating the Subscription?)


Reply to this email directly or view it on GitHub
#3 (comment)
.

My solution using Dagger 2 scopes is quite simple. RxPermissions is scoped by the application context (@AppScope) while Observable<Boolean> is scoped per activity with a .cache() operator applied. When #6 proxy activity is recreated on rotation, consumer activity and its scope is recreated too but the RxPermissions stays the same instance to receive the results. Although the new activity scope request a new observable from RxPermisssions, .cache() makes sure the result is stored before the consumer (a presenter in my case) is resubscribed. Once the presenter wakes up in onResume it subscribes to the observable and enjoys the result :D

@mttkay I almost used retained fragments, but I found I was actually writing more code than just solving it by hand.

Wow thank you all for taking your time to give help.

About making RxPermissions survives the activity lifecycle, a simple singleton seems good enough (I tested that in a branch).

As @dlew and I said, the main problem is really to rebind the subscription after the death/rebirth of the activity, and I'm more and more convinced this is not possible easily. The main goal of this library is to keep things simple and to provide Observables. So I start to think about trade-offs.

When you request a permission, the framework starts a activity called .permission.ui.GrantPermissionsActivity (I saw that in logcat), this activity shows a dialog and survives the rotation change. But if it doesn't survive, there is no more issue ! The user will have to re-trigger the permission request, but I don't think this is really embarrassing.
So the question is, can I alter that activity's lifecycle, or maybe more plausible ask it to kill itself when I detect the rotation change ?

WDYT ?

@tbruyelle I do not think force closing the permission dialog is a legit option. Also you are completely right with pointing out that the retriggering of the permission request is the one breaking the flow we want.

At work we ended up using a RxAppCompatActivity exposing an onPermisssionResultsReceivedObservable() . So we also broke the requestPermissions().subscribe() flow. I really don't think it is possible to keep it without having to attach/register and detach/unregister the activity on the RxPermission instance.

I haven't found time to use this library yet, but from what I see I have to agree with @Gi-lo ... I don't see another easy and reliable way ...

We could improve that: Instead of having a RxAppCompatActivity as base class we could do something super lazy and add an invisible Fragment (no UI) to get access to Fragment.onRequestPermissionsResult() callback (and other lifecycle events if needed). So we could change the api of this library to something like rxPermissions.with(activity).request(Manifest.permission.CAMERA) where withActivity(FragmentActivity) would attach the invisible fragment or reuse the already attached fragment. I think that would go into the direction @dlew was suggesting (i.e. saving the state into a bundle).

However, that would mean that this library would depend on Fragments and FragmentActivity which I really dislike in general.

Just release a new version 0.2.0 :

  • RxPermissions is now a singleton gettable with getInstance(Context). This fix the crash.
  • Add an internal activity to handle the onRequestPermissionsResult so it's no more needed to override this method in the user acitivity.

That said, the problem about detached subscriber on rotation change is still there.

I know that this is not good pattern, but what do you think about this:

After permission request is send we lock current rotation in activity that send request and ShadowActivity.

After we deliver permission repsonse to subscriber we unlock rotation.

@domsu Yeah this is something to which I already thought, and this is easy to implement with RxJava. We just add a doOnSubscribe(lock) and a doOnTerminate/doOnUnscubscibe(unlock) to the observable.

But it can be intrusive, maybe we could make it optional ? As you said it's not a good pattern, but if it shock anyone, I'm ready to implement it, as a recommended option.

This is not a good idea. There are other configuration changes than the orientation change, such as a language change or docking a hardware keyboard. This lib needs a solution which targets all configuration changes.
Locking the orientation is by the way not guaranteed. There are non root apps in the play store working around those restrictions.

Could the observable not be shared somehow by using one of rx java's share operations? In that case it wouldn't make any difference if the observable gets unsubscribed and resubscribed after screen orientation changes, right?

However, in the unlikely case that the activity goes in background before the user grants/denies permission, because the user starts another App (i.e. open a push notification), the hosting activity could be destroyed if the app goes in background ... Hence, I think that saving the instance state in a bundle is the right way to go ... I have something like restoring / recreating an Observable from Bundle and share that observable with multiple subscribers in mind.

This can't be solved.. If you shoot off another activity you always risk being garbage collected, that means the observable will disappear. You can't solve that as you cant serialize an observable mid stream.

@andaag Yep I agree. So 2 solutions, put the project to the trash or add hacky trade-offs...

I prefer the solution 2 because for me the project brings more good than harm.

it doesn't bother me to lock the rotation during the permission request. And if for some rooted devices it's not possible to lock the rotation, the users will have to request the permission twice, this doesn't matter.
For other configuration changes, like language change, same thing. I mean, it's not very common that a user changes the language while an app is requesting a permission.

By explicitly warning the library's users of these trade-offs, I think we can move forward.

I've been toying with the idea of creating some base Activity/Fragments that would allow you to observe all possible onEvents. With such a tool on hand, this wouldn't be nearly as bad:

  1. You could handle configuration changes in the background.
  2. You can automatically unsubscribe when the Activity dies.
  3. You can simply resume permission request on demand.

It would still require a re-subscription to the Observable, but that's a logical outcome of wanting to have an Observable last longer than the Activity it was created within.

@dlew Actually with the shadow activity introduced in version 0.2.0, we have such tool ! Nice.

About 3, I have no control on the runtime permission request (other than create a new one), I can't cancel or resume it. The permission request is resumed automatically after a configuration change.

Anyway, a solution begins to take shape :

  1. a permission request is triggered : RxPermissions registers a subject associated with that permission and requests the framework : the runtime permission popup appears.
  2. a configuration change occurs : RxPermissions unsubscribes all registered subjects, but keeps in memory the pending requests. It will survive the configuration change because it's a singleton since 0.2.0.
    The runtime permissions popup is paused and will be resumed automatically.
  3. the app restarts : The permission request is redone by the app (this is actually the only requirement, but as @dlew said it's a logical outcome), RxPermissions remembers a pending request has been done for this permission, and registers a new subject for it, but doesn't request the framework a second time (otherwise the result would be another runtime permissions popup).
    The runtime permissions popup is resumed and displayed again.
  4. user answers to the permission request : RxPermissions.onRequestPermissionsResult is invoked, the lib finds the corresponding subject and emits the user response.

That sounds not bad...

@dlew I would appreciate such an Activity/Fragment where you can observe every lifecycle method. Currently it's impossible to use composition over inheritance for fragments and activities. I saw/build many libs requiring the user to call a delegate in ~10 lifecycle methods to make them work or extend their LibXyzActivity. There was a RxFragment earlier in RxAndroid (not forwarding all lifecycle events and NonConfigurationInstance was missing) which could be a start. Personally I would build it without Rx in the first step to make it universal usable.

With a ComposableActivity we could write plugins which could be used to bind multiple libs logic to activities.

Libs that could profit from this:

This looks like a solution but is not maintained: https://github.com/BoD/android-activitylifecyclecallbacks-compat

I'll bump this up in my priorities and work out a draft this week. I think there could be a lot of benefit out there for it.

I once wrote a class called 'LifecycleExtension' where you have one method called 'onLifecycleEventReceived'. I used the RxLifecycleFragment provided in RXLifecycle to provide the correct events. Since that I was able to use composition over inheritence for everything that depends on the lifecycle eg. Butterknife, Tracking and much more. Also those Lifecycle objects are heavily testable. Long story short, a base Activity that has for every callback it implements an observable as well, is something that we should love to use.

@tbruyelle

  1. User clicks something that requires a permission
  2. Permission dialog pops up
  3. User leaves app to google what the permission means.
  4. Browser requires memory and app gets removed from active memory.
  5. User jumps back in app, clicks accept, but everything is serialized, and rx breaks.

This is a very special case, but it is a case... Might be worth it for some people to use rx anyway though, and know that that case wont be handled very well.

What we're going to do at my company to use rx for this kind of flow (locationsettings for example). Is to simply return an exception in onError in these cases. So one needs to manually support handling "out of activity" type actions.

One can stick a bundle in savedInstanceState and pickup that bundle in onActivityResult, that bundle should contain enough information to continue the rx mid flow (this means you'd need to have multiple chained observables for the original request).

Chances are we're gonnna post that bundle/an object on an event bus and pick it up in the same location that we do the original request. To keep the logic for handling it in the same place.

Interesting, but I'm not sure to understand how things are connected together.

We wrote code for this for changing location settings, and I've now used the approach for permissions as well. Since we've code the code already written I'll stick it into a library and put it on github probably sometime tomorrow.

It's the best workaround to the problem of serialization I've been able to come up with atleast.

//Edit : very open for suggestions for names on this as well :p. RxActivityRestart/RxWithSerialization?...

Nice, please link the library when it's ready.
About the name, sorry I have no idea.

https://bintray.com/finnandroid/maven/rxactivityresponse/0.1/view#files
https://github.com/finn-no/rxactivityresponse

I'll push out a library to support google play settings next week. So one can do things like

RxLocation.get(restartHandler).subscribe(v -> {})

And get a flow that first asks for location, then asks for gps enabled, then gets a location and returns it. Safe from activity restarts/rotations halfway through.

We would have made this a PR, but it's an entirely different approach. With it's own pro's and con's. (Pro's being it'll always work, con being it's more work to support it. Chances are some users would be happy to cover the 95% case). However we'll be using this solution to solve other problems as well. For example getting a picture from the gallery, where your app is fairly likely to be dumped from memory halfway through the process.

I pushed a PR #14 that aims to fix the issue. I would appreciate if some of you review it.

The fix removes some flexibility to the lib but I didn't find anything better.

The good news is for clients who request the permission during activity/fragment/view start, nothing change, it will work as these.
For client who request the permission after a particular event, there is some breaking changes :

  • Firstly, they need to move the request call in one of the activity/fragment/view start method.
  • Secondly, they need to add an extra parameter to the request or requestEach method : an Observable that represents that particular event. See the PR for details.

Example from the sample app, the click event is transformed to an observable with RxBinding :

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.act_main);

         // com.jakewharton.rxbinding.view.RxView is perfect to represent the event needed
        Observable<Object> trigger = RxView.clicks(findViewById(R.id.enableCamera));

         RxPermissions.getInstance(this)
                .request(trigger, Manifest.permission.CAMERA)
                .subscribe(granted -> {

@andaag Thanks for the help but I think I found a solution which bring less changes on the client side.

@tbruyelle Hm, that would work, but it puts some pretty big limits on code structure. We have an app with 100+ views (using views instead of fragments as well), and having all of our permission related code in the single activity onCreate isn't really an option. It is considerably easier for those where that is an option though.

For just glancing at the code, I'm guessing your using the permission itself as an identifier for the request? That means you can only request a permission from one place in the app. If you want the camera permission in 3 different places your gonna need another identifier.

For just glancing at the code, I'm guessing your using the permission itself as an identifier for the request? That means you can only request a permission from one place in the app. If you want the camera permission in 3 different places your gonna need another identifier.

I think it should work. From the beginning the lib has been designed with concurrent access in focus. For instance, if there's 3 camera permission requests from 3 different places, the same observable is returned to the subscribers. So when the user's answer is caught, all subscribers will receive it.

That said, if the requests are done very simultaneously, it won't work as expected since I forgot to use synchronized... That's another small improvement.

I uploaded a snapshot version

compile 'com.tbruyelle.rxpermissions:rxpermissions:0.4.0@aar'

@andaag I tested a little more and it works also if you make the permission request in View.onFinishInflate or any methods invoked during the app initialization.

So you're not limited to onCreate/onResume.

Right you are... That.. is a better solution.

I had a look at it, and overall it looks fine, but since the singleton is a static referanse and you never remove anything from mSubjects it'll leak. I dont see any other choice here but to remove from that on unsubscribe and require clients to implement that. (Or hold a weak ref to subscribers, but that'll require the activity to hold an active referance to it or be GC'd. Cant be an inner referance).

Since this is a better approach I'd like to extend this to support onActivityResult as well. Making a seperate issue for that though to keep this on task. See #15

you never remove anything from mSubjects it'll leak.

When the lib receives the permission request answer, the subject receives onComplete which unsubscribe all subscribers, and is removed from the list.
https://github.com/tbruyelle/RxPermissions/blob/lifecycle-handling/lib/src/main/java/com/tbruyelle/rxpermissions/RxPermissions.java#L274

I'm on my phone right now, so reviewing code is a bit tricky. I'm talking about the case where the user doesn't trigger the action that requires the permission. In that case you are holding the observer which is an inner function holding the activity right?

Yes you're right, I probably should make RxPermissions.onDestroy() public, to unsubscribe all subjects.

That'll break if you use it in fragments with the same activity though.

I guess it's possible to make a destroy that takes the view / fragment / activity as id and destroy(id), but unsubscribe might be just as clean... Not sure really.

Also, another idea... Just an idea, I can't really decide whether or not I like it better.

If instead of a trigger you hold the subscription. Then you could make the observable a field, and in onClick trigger a .subscribe. Then you dont need RxView for anything.

I can't decide if I like this or not, but since I can't decide I figured I'd mention it and you can think it over as well :p. I think it might be better at dealing with special cases, for example if you want to immediately start showing a spinner, then request permission or something like that.

Also, currently the branch doesn't allow you to click the button more than once. Not an issue with "turn camera on" functionality, but it'll break a whole lot of other flows.

@andaag Thanks for the report, I noticed that bug last friday. The issue is due to the take(1) which prevents the trigger observable to emit more than one item. I removed it here 2455bcf but now I have to fix the request method which doesn't work any more.

Indeed, take(1) was here to ensure the sequence was completed, this is mandatory because request use toList() to transform the original sequence to a single boolean (toList() requires completed sequence or else it emits nothing).

I have an idea of the final fix, I'll publish it today.

So version 0.4.1 brings some other fixes, including the one mentioned by @andaag

compile 'com.tbruyelle.rxpermissions:rxpermissions:0.4.1@aar'

I think I'll soon merge the PR #14