florent37/RuntimePermission

Crash in fragment when screen rotates

alexChurkin opened this issue · 2 comments

Hello! I found an issue with using your library in fragments. Try to run the code below and rotate your screen, and you'll see an exception like this:

2018-11-24 04:05:54.640 20891-20891/com.test.myapplication E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.test.myapplication, PID: 20891
    java.lang.RuntimeException: Failure delivering result ResultInfo{who=@android:requestPermissions:, request=65559, result=-1, data=Intent { act=android.content.pm.action.REQUEST_PERMISSIONS (has extras) }} to activity {com.test.myapplication/com.test.myapplication.MainActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String android.content.Context.getPackageName()' on a null object reference
        at android.app.ActivityThread.deliverResults(ActivityThread.java:4089)
        at android.app.ActivityThread.handleSendResult(ActivityThread.java:4132)
        at android.app.ActivityThread.-wrap20(ActivityThread.java)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1533)
        at android.os.Handler.dispatchMessage(Handler.java:102)
        at android.os.Looper.loop(Looper.java:154)
        at android.app.ActivityThread.main(ActivityThread.java:6119)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
     Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String android.content.Context.getPackageName()' on a null object reference
        at android.widget.Toast.<init>(Toast.java:103)
        at android.widget.Toast.makeText(Toast.java:256)
        at com.test.myapplication.TestFragment.showToast(TestFragment.kt:36)
        at com.tes.myapplication.TestFragment$doIt$2.invoke(TestFragment.kt:29)
        at com.test.myapplication.TestFragment$doIt$2.invoke(TestFragment.kt:14)
        at com.github.florent37.runtimepermission.kotlin.KotlinRuntimePermission$onDeclined$1.onResponse(kotlin-runtimepermissions.kt:29)
        at com.github.florent37.runtimepermission.RuntimePermission.onReceivedPermissionResult(RuntimePermission.java:123)
        at com.github.florent37.runtimepermission.RuntimePermission.access$000(RuntimePermission.java:27)
        at com.github.florent37.runtimepermission.RuntimePermission$1.onRequestPermissionsResult(RuntimePermission.java:47)
        at com.github.florent37.runtimepermission.PermissionFragment.onRequestPermissionsResult(PermissionFragment.java:89)
        at androidx.fragment.app.FragmentActivity.onRequestPermissionsResult(FragmentActivity.java:860)
        at android.app.Activity.dispatchRequestPermissionsResult(Activity.java:7084)
        at android.app.Activity.dispatchActivityResult(Activity.java:6936)
        at android.app.ActivityThread.deliverResults(ActivityThread.java:4085)
        at android.app.ActivityThread.handleSendResult(ActivityThread.java:4132) 
        at android.app.ActivityThread.-wrap20(ActivityThread.java) 
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1533) 
        at android.os.Handler.dispatchMessage(Handler.java:102) 
        at android.os.Looper.loop(Looper.java:154) 
        at android.app.ActivityThread.main(ActivityThread.java:6119) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)

My activity:

package com.test.myapplication

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.container)

        if (savedInstanceState == null) {
            supportFragmentManager
                .beginTransaction()
                .replace(R.id.container, TestFragment())
                .commit()
        }
    }
}

My fragment:

package com.test.myapplication

import android.Manifest
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import com.github.florent37.runtimepermission.kotlin.askPermission
import kotlinx.android.synthetic.main.testfragment.*
import kotlinx.android.synthetic.main.testfragment.view.*

class TestFragment : Fragment() {

    private var toast: Toast? = null

    lateinit var button: Button

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        val view = inflater.inflate(R.layout.testfragment, container, false)
        button = view.button
        button.setOnClickListener { doIt() }
        return view
    }

    fun doIt() {
        askPermission(Manifest.permission.READ_CONTACTS, Manifest.permission.ACCESS_FINE_LOCATION) {
            showToast("ACCEPTED")
            button.text = "ACCEPTED"
        }.onDeclined {
            showToast("DECLINED")
            button.text = "DECLINED"
        }
    }

    fun showToast(text: String) {
        toast?.cancel()
        toast = Toast.makeText(context, text, Toast.LENGTH_SHORT).apply { show() }
    }
}

I tried to create my own library to make requesting permissions easier and came across the same crash :/

@alexChurkin this is the expected behavior in your case.
When you rotate the device, your MainActivity and TestFragment are recreated. You pass lambdas (accepted and declined blocks) as the askPermission method call parameters that are referenced to the Fragment they are created in. Then you rotate the device, and you callback body will be invoked in the already detached TestFragment. That's the reason why context is null. By the way, this is a memory leak.

This issue also occurs in the example Activities. The only difference is the absence of crashes)
appendText(resultView, "Denied :") code does nothing actually (it is invoked into the leaked Activity)

You should handle this case in your own: use ViewModel, Loaders etc.