Android Question Multiple Runtime Permissions / Wait For.. Complete not working? RESOLVED

SimonInCanada

Member
Licensed User
I am trying to update an app that was based on the old Bluetooth example so may not take advantage of all the latest developments in program structure. I do not write programs for B4A very often as I mostly have to use Embedded C or design electronics. My apps are used to support our embedded systems in the field.

This app needs to support Android 6+ Runtime permissions. The appropriate tutorial has an example of using RuntimePermissions.CheckandRequest() in conjunction with Wait For but does not illustrate setting multiples, and the technique doesn't work because my app runs into a run time error (from code foollowing the call to PermissionsTake1 that accesses the external storage) before the permissions dialog is even displayed. This is the sub PermissionsTake1.

I tried changing to the technique using PermissionsTake2 and a resumable Sub MyPermission so I could call MyPermission with the Resumable Subs Tutorial's Wait For () Complete (Result as boolean).

In neither case do the permissions dialogs get a chance to run but I cannot see how to restructure my program not to read the ini file in Activity_Create or shortly thereafter.

B4X:
Sub PermissionsTake1
    ' 22-07-2019 access dangerous permissions for Android 6+
    Log ("Checking PERMISSION_WRITE_EXTERNAL_STORAGE")
    rp.CheckAndRequest(rp.PERMISSION_WRITE_EXTERNAL_STORAGE)
    Wait For Activity_PermissionResult (Permission As String, Result As Boolean)
    Log ("Finished checking PERMISSION_WRITE_EXTERNAL_STORAGE")
    If Result = False Then
        ToastMessageShow("Unable to set up PERMISSION_WRITE_EXTERNAL_STORAGE", True)
        ExitApplication
    End If
   
    Log ("Checking PERMISSION_ACCESS_COARSE_LOCATION")
    rp.CheckAndRequest(rp.PERMISSION_ACCESS_COARSE_LOCATION)
    Wait For Activity_PermissionResult (Permission As String, Result As Boolean)
    Log ("Finished checking PERMISSION_ACCESS_COARSE_LOCATION")
    If Result = False Then
        ToastMessageShow("Unable to set up PERMISSION_ACCESS_COARSE_LOCATION", True)
        ExitApplication
    End If

    Log ("Checking PERMISSION_ACCESS_FINE_LOCATION")
    rp.CheckAndRequest(rp.PERMISSION_ACCESS_FINE_LOCATION)
    Wait For Activity_PermissionResult (Permission As String, Result As Boolean)
    Log ("Finished checking PERMISSION_ACCESS_FINE_LOCATION")
    If Result = False Then
        ToastMessageShow("Unable to set up PERMISSION_ACCESS_FINE_LOCATION", True)
        ExitApplication
    Else
        PermissionsDone = True
    End If
   
End Sub

Sub PermissionsTake2
    Log ("Checking PERMISSION_WRITE_EXTERNAL_STORAGE")
    Wait For (MyPermission(rp.PERMISSION_WRITE_EXTERNAL_STORAGE)) Complete (Result As Boolean)
    Log ("Finished checking PERMISSION_WRITE_EXTERNAL_STORAGE")
    If Result = False Then
        ToastMessageShow("Unable to set up PERMISSION_WRITE_EXTERNAL_STORAGE", True)
        ExitApplication
    End If

    Log ("Checking PERMISSION_ACCESS_COARSE_LOCATION")
    Wait For (MyPermission(rp.PERMISSION_ACCESS_COARSE_LOCATION)) Complete (Result As Boolean)
    Log ("Finished checking PERMISSION_ACCESS_COARSE_LOCATION")
    If Result = False Then
        ToastMessageShow("Unable to set up PERMISSION_ACCESS_COARSE_LOCATION", True)
        ExitApplication
    End If

    Log ("Checking PERMISSION_ACCESS_FINE_LOCATION")
    Wait For (MyPermission(rp.PERMISSION_ACCESS_FINE_LOCATION)) Complete (Result As Boolean)
    Log ("Finished checking PERMISSION_ACCESS_FINE_LOCATION")
    If Result = False Then
        ToastMessageShow("Unable to set up PERMISSION_ACCESS_FINE_LOCATION", True)
        ExitApplication
    End If
   
End Sub


' in theory we can make a blocking call To this by Wait For OnePermission(rp.PermName) Complete (Result As Boolean)
Sub MyPermission(Permname As String) As ResumableSub
    Log("++MyPermission")
    rp.CheckAndRequest(Permname)
    Wait For Activity_PermissionResult (Permission As String, Result As Boolean)
    Log("--MyPermission")
    Return Result

End Sub
 

Attachments

  • BluetoothWithPermission.zip
    50.3 KB · Views: 346

Erel

B4X founder
Staff member
Licensed User
Longtime User
Upvote 0

Andrew (Digitwell)

Well-Known Member
Licensed User
Longtime User
@SimonatMSL,
I think that you have 2 problems

1. the calling sub


B4X:
sub domywork
   PermissionTake1
   'continue with other code, which crashes, because the permissions have not been set.
end sub

The issue is that PermissionTake1 returns to domywork as soon as the first Wait For is reached. This is expected behaviour of using Wait for

You need to do something like:

B4X:
sub domywork
  Wait for (PermissionTake1) complete (ret as object) ' or boolean or whatever
 'continue with other code, this will now wait until permissionTake1 completes
end sub

2. ExitApplication

I don't believe that exitapplication closes the app immediately. I think that if there are messages on the queue these are processed.
So the code above would still not work properly since the app does not actually exit until control is returned to the OS.

My recommendation is this:

B4X:
sub domywork
   PermissionTake2
   ' do nothing else here return to OS

end sub

sub continuewithapp
' this is where you can get on and do any work, knowing that all the permissions succeeded
end sub

Sub PermissionsTake2 as resumeablesub
    Log ("Checking PERMISSION_WRITE_EXTERNAL_STORAGE")
    Wait For (MyPermission(rp.PERMISSION_WRITE_EXTERNAL_STORAGE)) Complete (Result As Boolean)
    Log ("Finished checking PERMISSION_WRITE_EXTERNAL_STORAGE")
    If Result = False Then
        ToastMessageShow("Unable to set up PERMISSION_WRITE_EXTERNAL_STORAGE", True)
        ExitApplication
        return false
    End If

    Log ("Checking PERMISSION_ACCESS_COARSE_LOCATION")
    Wait For (MyPermission(rp.PERMISSION_ACCESS_COARSE_LOCATION)) Complete (Result As Boolean)
    Log ("Finished checking PERMISSION_ACCESS_COARSE_LOCATION")
    If Result = False Then
        ToastMessageShow("Unable to set up PERMISSION_ACCESS_COARSE_LOCATION", True)
        ExitApplication 
       return false
    End If

    Log ("Checking PERMISSION_ACCESS_FINE_LOCATION")
    Wait For (MyPermission(rp.PERMISSION_ACCESS_FINE_LOCATION)) Complete (Result As Boolean)
    Log ("Finished checking PERMISSION_ACCESS_FINE_LOCATION")
    If Result = False Then
        ToastMessageShow("Unable to set up PERMISSION_ACCESS_FINE_LOCATION", True)
        ExitApplication
        return false
    End If

    ' Let the app know it can continue
    callsub(me,"continuewithapp")
  return true
End Sub

I haven't checked this in the IDE.I have just typed it in here. But I think this should solve your problem. You don't actually need to return a boolean.
 
Upvote 0

SimonInCanada

Member
Licensed User
@SimonatMSL,
I think that you have 2 problems

1. the calling sub


B4X:
sub domywork
   PermissionTake1
   'continue with other code, which crashes, because the permissions have not been set.
end sub

The issue is that PermissionTake1 returns to domywork as soon as the first Wait For is reached. This is expected behaviour of using Wait for

You need to do something like:

B4X:
sub domywork
  Wait for (PermissionTake1) complete (ret as object) ' or boolean or whatever
 'continue with other code, this will now wait until permissionTake1 completes
end sub

2. ExitApplication

I don't believe that exitapplication closes the app immediately. I think that if there are messages on the queue these are processed.
So the code above would still not work properly since the app does not actually exit until control is returned to the OS.

My recommendation is this:

B4X:
sub domywork
   PermissionTake2
   ' do nothing else here return to OS

end sub

sub continuewithapp
' this is where you can get on and do any work, knowing that all the permissions succeeded
end sub

Sub PermissionsTake2 as resumeablesub
    Log ("Checking PERMISSION_WRITE_EXTERNAL_STORAGE")
    Wait For (MyPermission(rp.PERMISSION_WRITE_EXTERNAL_STORAGE)) Complete (Result As Boolean)
    Log ("Finished checking PERMISSION_WRITE_EXTERNAL_STORAGE")
    If Result = False Then
        ToastMessageShow("Unable to set up PERMISSION_WRITE_EXTERNAL_STORAGE", True)
        ExitApplication
        return false
    End If

    Log ("Checking PERMISSION_ACCESS_COARSE_LOCATION")
    Wait For (MyPermission(rp.PERMISSION_ACCESS_COARSE_LOCATION)) Complete (Result As Boolean)
    Log ("Finished checking PERMISSION_ACCESS_COARSE_LOCATION")
    If Result = False Then
        ToastMessageShow("Unable to set up PERMISSION_ACCESS_COARSE_LOCATION", True)
        ExitApplication
       return false
    End If

    Log ("Checking PERMISSION_ACCESS_FINE_LOCATION")
    Wait For (MyPermission(rp.PERMISSION_ACCESS_FINE_LOCATION)) Complete (Result As Boolean)
    Log ("Finished checking PERMISSION_ACCESS_FINE_LOCATION")
    If Result = False Then
        ToastMessageShow("Unable to set up PERMISSION_ACCESS_FINE_LOCATION", True)
        ExitApplication
        return false
    End If

    ' Let the app know it can continue
    callsub(me,"continuewithapp")
  return true
End Sub

I haven't checked this in the IDE.I have just typed it in here. But I think this should solve your problem. You don't actually need to return a boolean.


Hi

Thanks for your suggestions but they don't work. See my Reply to Erel

Simon
 
Upvote 0

SimonInCanada

Member
Licensed User
1. No reason to request both ACCESS_FINE_LOCATION and ACCESS_COARSE location. Only FINE is needed.

2. Simpler way to request multiple permissions: https://www.b4x.com/android/forum/threads/handle-multiple-permission-request.94611/#post-598788

3. If you want to call this sub then you must make it return ResumableSub and call it with Wait For: [B4X] Resumable subs that return values (ResumableSub)

Thank you
Your suggestion (2) is nice but it won'I have already read the thread (3) and tried what it suggests.

This is the error Log now that I made some changes

B4X:
** Activity (main) Create, isFirst = true **
Checking PERMISSION_WRITE_EXTERNAL_STORAGE
** Activity (main) Resume **
Executing Activity_Resume
Error occurred on line: 60 (Common)
java.io.FileNotFoundException: /storage/emulated/0/Android/data/com.msluk.net.clearwell_handheld/files/Bluetooth.ini: open failed: ENOENT (No such file or directory)
    at libcore.io.IoBridge.open(IoBridge.java:452)
    at java.io.FileOutputStream.<init>(FileOutputStream.java:87)
    at anywheresoftware.b4a.objects.streams.File.OpenOutput(File.java:448)
    at anywheresoftware.b4a.objects.streams.File.WriteMap(File.java:290)
    at com.msluk.net.clearwell_handheld.common._loadndefaultmap(common.java:276)
    at com.msluk.net.clearwell_handheld.common._initmap(common.java:111)
    at com.msluk.net.clearwell_handheld.main._activity_resume(main.java:820)
    at java.lang.reflect.Method.invoke(Native Method)
    at anywheresoftware.b4a.shell.Shell.runMethod(Shell.java:732)
    at anywheresoftware.b4a.shell.Shell.raiseEventImpl(Shell.java:348)
    at anywheresoftware.b4a.shell.Shell.raiseEvent(Shell.java:255)
    at java.lang.reflect.Method.invoke(Native Method)
    at anywheresoftware.b4a.ShellBA.raiseEvent2(ShellBA.java:144)
    at anywheresoftware.b4a.BA.raiseEvent(BA.java:176)
    at com.msluk.net.clearwell_handheld.main.afterFirstLayout(main.java:110)
    at com.msluk.net.clearwell_handheld.main.access$000(main.java:17)
    at com.msluk.net.clearwell_handheld.main$WaitForLayout.run(main.java:82)
    at android.os.Handler.handleCallback(Handler.java:739)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:148)
    at android.app.ActivityThread.main(ActivityThread.java:7406)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1230)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1120)
Caused by: android.system.ErrnoException: open failed: ENOENT (No such file or directory)
    at libcore.io.Posix.open(Native Method)
    at libcore.io.BlockGuardOs.open(BlockGuardOs.java:186)
    at libcore.io.IoBridge.open(IoBridge.java:438)
    ... 23 more
** Activity (main) Pause, UserClosed = false **

This is the changed code

B4X:
Sub Activity_Create(FirstTime As Boolean)

    If FirstTime Then
        admin.Initialize("admin")
        serial1.Initialize("serial1")
        ListFiles.Initialize
        foundDevices.Initialize
        RestartTimer.Initialize("RestartTimer", 1000)
        RestartTimer.Enabled = True
    End If

    Activity.LoadLayout("1")
    
    Wait for (PermissionsTake1) Complete (Result As Boolean)
    Log ("Returned from Permissions")

    Common.RestartApp = False
    ' init the Map to be used like an ini file   
'    If (FirstTime) Then
'        Common.InitMap
'        Common.InitAliases
'    End If

    ' this button was misbehaving
    btnCancel.Enabled = True
    
    Common.KeepScreenOn(True)
    
End Sub

' This gets called at the start and after a connect session
Sub Activity_Resume
    Log("Executing Activity_Resume")
    Common.InitMap
    Common.InitAliases

    ' back to original code
    btnSearchForDevices.Enabled = False
    If admin.IsEnabled = False Then
        If admin.Enable = False Then
            ToastMessageShow("Error enabling Bluetooth adapter.", True)
        Else
            ToastMessageShow("Enabling Bluetooth adapter...", False)
            'the StateChanged event will be soon raised
        End If
    Else
        Admin_StateChanged(admin.STATE_ON, 0)
    End If
    LoadLogFiles

    Common.ReadMapFromSD
    StopAnimation
    
    If Common.MyMap.Get(Common.mapAutoCap) Then
        Common.ScreenShot(Activity.Width, Activity.Height, "Main")
    End If
    
End Sub

Sub PermissionsTake1 As ResumableSub
    Dim PermissionsDone As Boolean = False
    ' 22-07-2019 access dangerous permissions for Android 6+
    Log ("Checking PERMISSION_WRITE_EXTERNAL_STORAGE")
    rp.CheckAndRequest(rp.PERMISSION_WRITE_EXTERNAL_STORAGE)
    Wait For Activity_PermissionResult (Permission As String, Result As Boolean)
    Log ("Finished checking PERMISSION_WRITE_EXTERNAL_STORAGE")
    If Result = False Then
        ToastMessageShow("Unable to set up PERMISSION_WRITE_EXTERNAL_STORAGE", True)
        ExitApplication
    End If
    
    Log ("Checking PERMISSION_ACCESS_COARSE_LOCATION")
    rp.CheckAndRequest(rp.PERMISSION_ACCESS_COARSE_LOCATION)
    Wait For Activity_PermissionResult (Permission As String, Result As Boolean)
    Log ("Finished checking PERMISSION_ACCESS_COARSE_LOCATION")
    If Result = False Then
        ToastMessageShow("Unable to set up PERMISSION_ACCESS_COARSE_LOCATION", True)
        ExitApplication
    End If

    Log ("Checking PERMISSION_ACCESS_FINE_LOCATION")
    rp.CheckAndRequest(rp.PERMISSION_ACCESS_FINE_LOCATION)
    Wait For Activity_PermissionResult (Permission As String, Result As Boolean)
    Log ("Finished checking PERMISSION_ACCESS_FINE_LOCATION")
    If Result = False Then
        ToastMessageShow("Unable to set up PERMISSION_ACCESS_FINE_LOCATION", True)
        ExitApplication
    Else
        PermissionsDone = True
    End If
    Return PermissionsDone
End Sub
 
Upvote 0

Erel

B4X founder
Staff member
Licensed User
Longtime User
Your code is wrong. The main mistake is to try to request the permissions when your app starts instead of ALWAYS requesting them right before you need them.
You also haven't removed the COARSE permission code. As I wrote, it is not needed.

As you can see in the logs, Activity_Resume runs before the permission was granted. Why are you reading the file in Activity_Resume? Looks like a mistake.

Correct code:
B4X:
Sub Activity_Create
 ...
 
 rp.CheckAndRequest(rp.PERMISSION_WRITE_EXTERNAL_STORAGE)
 Wait For Activity_PermissionResult (Permission As String, Result As Boolean)
 If Result Then
   Common.ReadMapFromSD
 Else
   ToastMessageShow("No permission to read from external storage", True)
   Activity.Finish
 End If
 
Upvote 0

SimonInCanada

Member
Licensed User
Post the code based on my example that doesn't work for you.
If possible create a small project and upload it.

Hi

I just placed the code in Activity_Resume as in the example and I got this

B4X:
** Activity (main) Create, isFirst = true **
** Activity (main) Resume **
Executing Activity_Resume
** Activity (main) Pause, UserClosed = false **
Error occurred on line: 50 (Common)
java.io.FileNotFoundException: /storage/emulated/0/Android/data/com.msluk.net.clearwell_handheld/files/Bluetooth.ini: open failed: ENOENT (No such file or directory)
    at libcore.io.IoBridge.open(IoBridge.java:452)
    at java.io.FileOutputStream.<init>(FileOutputStream.java:87)
    at anywheresoftware.b4a.objects.streams.File.OpenOutput(File.java:448)
    at anywheresoftware.b4a.objects.streams.File.WriteMap(File.java:290)
    at com.msluk.net.clearwell_handheld.common._writemaptosd(common.java:85)
    at com.msluk.net.clearwell_handheld.main._activity_pause(main.java:539)
    at java.lang.reflect.Method.invoke(Native Method)
    at anywheresoftware.b4a.shell.Shell.runMethod(Shell.java:732)
    at anywheresoftware.b4a.shell.Shell.raiseEventImpl(Shell.java:348)
    at anywheresoftware.b4a.shell.Shell.raiseEvent(Shell.java:255)
    at java.lang.reflect.Method.invoke(Native Method)
    at anywheresoftware.b4a.ShellBA.raiseEvent2(ShellBA.java:144)
    at com.msluk.net.clearwell_handheld.main.onPause(main.java:271)
    at android.app.Activity.performPause(Activity.java:7061)
    at android.app.Instrumentation.callActivityOnPause(Instrumentation.java:1340)
    at android.app.ActivityThread.performPauseActivity(ActivityThread.java:4654)
    at android.app.ActivityThread.performPauseActivity(ActivityThread.java:4627)
    at android.app.ActivityThread.handlePauseActivity(ActivityThread.java:4602)
    at android.app.ActivityThread.access$1300(ActivityThread.java:229)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1832)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loop(Looper.java:148)
    at android.app.ActivityThread.main(ActivityThread.java:7406)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1230)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1120)
Caused by: android.system.ErrnoException: open failed: ENOENT (No such file or directory)
    at libcore.io.Posix.open(Native Method)
    at libcore.io.BlockGuardOs.open(BlockGuardOs.java:186)
    at libcore.io.IoBridge.open(IoBridge.java:438)
    ... 25 more

The active code is now...

B4X:
Sub Activity_Create(FirstTime As Boolean)

    If FirstTime Then
        admin.Initialize("admin")
        serial1.Initialize("serial1")
        ListFiles.Initialize
        foundDevices.Initialize
        RestartTimer.Initialize("RestartTimer", 1000)
        RestartTimer.Enabled = True
    End If

    Activity.LoadLayout("1")
    
    'Wait for (PermissionsTake1) Complete (Result As Boolean)
    'Log ("Returned from Permissions")

    Common.RestartApp = False
    ' init the Map to be used like an ini file   
'    If (FirstTime) Then
'        Common.InitMap
'        Common.InitAliases
'    End If

    ' this button was misbehaving
    btnCancel.Enabled = True
    
    Common.KeepScreenOn(True)
    
End Sub

' This gets called at the start and after a connect session
Sub Activity_Resume
    Log("Executing Activity_Resume")
    
    For Each permission As String In Array(rp.PERMISSION_WRITE_EXTERNAL_STORAGE, rp.PERMISSION_ACCESS_FINE_LOCATION)
        rp.CheckAndRequest(permission)
        Wait For Activity_PermissionResult (permission As String, Result As Boolean)
        If Result = False Then
            ToastMessageShow("No permission!", True)
            Activity.Finish
            Return
        End If
    Next

    
    Common.InitMap
    Common.InitAliases

    ' back to original code
    btnSearchForDevices.Enabled = False
    If admin.IsEnabled = False Then
        If admin.Enable = False Then
            ToastMessageShow("Error enabling Bluetooth adapter.", True)
        Else
            ToastMessageShow("Enabling Bluetooth adapter...", False)
            'the StateChanged event will be soon raised
        End If
    Else
        Admin_StateChanged(admin.STATE_ON, 0)
    End If
    LoadLogFiles

    Common.ReadMapFromSD
    StopAnimation
    
    If Common.MyMap.Get(Common.mapAutoCap) Then
        Common.ScreenShot(Activity.Width, Activity.Height, "Main")
    End If
    
End Sub

I am sorry that I have not created a simpler project yet.

You also haven't removed the COARSE permission code. As I wrote, it is not needed
As you can see I have removed it now.

As you can see in the logs, Activity_Resume runs before the permission was granted. Why are you reading the file in Activity_Resume? Looks like a mistake.

I tried your suggestion in Activity_Create but again it just ends up displaying the permissions dialog after the program has crashed due to EOENT (much the same debug Log). The program was strructured this way because of the order in which the objects are initialised (e.g. the Map for the ini file) and the file is read very early on so that the rest of the program can rely on the values. It may not be optimal but it has always worked fine and as I said, I was hoping not to have to restructure very much code because I am so busy and there is just one customer who is not using one of our company phones. I cannot understand why the Resumable Subs with Complete still seem to return right away.
 
Upvote 0

DonManfred

Expert
Licensed User
Longtime User
The active code is now...
The code does not match the error. Or at least one can not see the problem in the code you posted.
Error occurred on line: 50 (Common)
java.io.FileNotFoundException: /storage/emulated/
0/Android/data/com.msluk.net.clearwell_handheld/files/Bluetooth.ini: open failed: ENOENT (No such file or directory)

/files/Bluetooth.ini

files in the files folder must be lowercased.
 
Upvote 0

Erel

B4X founder
Staff member
Licensed User
Longtime User
I tried your suggestion in Activity_Create but again it just ends up displaying the permissions dialog after the program has crashed due to EOENT
I can only guess that you are still trying to access the external storage from Activity_Resume.
Don't. Request the permission in Activity_Create and access the external storage after you get the permission.

See the bold message here: https://www.b4x.com/android/forum/threads/handle-multiple-permission-request.94611/#post-598788
 
Upvote 0

SimonInCanada

Member
Licensed User
I can only guess that you are still trying to access the external storage from Activity_Resume.
Don't. Request the permission in Activity_Create and access the external storage after you get the permission.

See the bold message here: https://www.b4x.com/android/forum/threads/handle-multiple-permission-request.94611/#post-598788

Okay, thanks, moving all the code to Activity_Create did almost resolve it. I also had to remove any code that accessed the filing system from Activity_Pause. I can also reliably avoid Activity_Resume or Activity_Pause from accessing filing until I set a global variable at the end of Activity_Create.

I think I was most confused because it did not seem right to me that Activity_Resume would get triggered until after Activity_Create is 100% finished, especially when we are using the Wait For (x) Complete (y) construct.

Thanks for your patience!
 
Upvote 0

OliverA

Expert
Licensed User
Longtime User
It would probably be good to point this issue out in the "Android Process and activities life cycle" tutorial (https://www.b4x.com/android/forum/threads/android-process-and-activities-life-cycle.6487/). It makes sense that Android does not realize that the return from a Wait For is not the actual final return and therefore calls the next life cycle stage in line, even before the previous one finishes for real, but it would be good if the tutorial would make programmers aware of that.
 
Upvote 0
Top