shinyorg / shiny

.NET Framework for Backgrounding & Device Hardware Services (iOS, Android, & Catalyst)
https://shinylib.net
MIT License
1.43k stars 227 forks source link

[Bug]: await IBleManager.RequestAccessAsync() never returns a completed task the first time asking for permissions #1427

Closed vchelaru closed 6 months ago

vchelaru commented 6 months ago

Component/Nuget

BluetoothLE Client (Shiny.BluetoothLE)

What operating system(s) are effected?

Version(s) of Operation Systems

Android 13.0

Hosting Model

Steps To Reproduce

Note - this requires that the app is run for the first time. If running subsequent times, the app should be uninstalled so that permissions are reset.

  1. Initialize Shiny BLE using the code shown below
    
    public static MauiApp CreateMauiApp()
    {
    var builder = MauiApp.CreateBuilder();
    builder
        .UseMauiApp<App>()
        .UseShiny()
        .ConfigureFonts(fonts =>
        {
            fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
        });
    builder.Services.AddBluetoothLE();
    return builder.Build();
    }
2. Add a button to a screen
3. Handle the Clicked event (either in XAML or through custom code) 
4. In the (async) method handler, call `RequestAccessAsync`
5. Place a breakpoint on the RequestAccessAsync to verify that it gets hit, and a breakpoint on the following line to verify that it is not hit.
![image](https://github.com/shinyorg/shiny/assets/729631/409d5112-4fd7-4788-a3fb-071b58b70e5a)
6. Run the application
7. Click the button to request access
8. Observe - the first breakpoint on the RequestAccessAsync call is hit
9. Click to continue in Visual Studio 
9. Observe - the popup for requesting bluetooth access appears on the app
10. Click to allow access

Observe: The second breakpoint is never hit
Expected: After the user accepts the popup, the next breakpoint should get hit with the proper AccessState

The following animation shows a debugging session. Note that the device is displayed in a gif, but this is a mirror of an actual hardware device - this is not an emulator.
![05_12 35 13](https://github.com/shinyorg/shiny/assets/729631/6c8dfdae-fd62-44dc-b122-6349a1cb24a7)

Additional Info: 
* This was tested on a Pixel 4 with Android 13
* If the app is restarted, the access state is remembered. Subsequent runs of the app (without uninstalling) do not result in a popup, and access is set to AccessState.Available.
* Clicking Allow and Don't Allow has no impact on the initial behavior - the task does not complete in either case.
* The following output appears when permissions are accepted:

[CompatibilityChangeReporter] Compat change id reported: 78294732; UID 10303; state: ENABLED


### Expected Behavior

The task should complete, allowing execution of the application to continue.

### Actual Behavior

The task hangs indefinitely.

### Exception or Log output

As mentioned above:

[CompatibilityChangeReporter] Compat change id reported: 78294732; UID 10303; state: ENABLED



### Code Sample

https://github.com/vchelaru/BleBug

### Code of Conduct

- [X] I have supplied a reproducible sample that is NOT FROM THE SHINY SAMPLES!
- [X] I am a Sponsor OR I am using the LATEST stable/beta version from nuget (v3.0 stable - ALPHAS are not taking issues - Sponsors can still send v2 issues)
- [X] I am Sponsor OR My GitHub account is 30+ days old
- [X] I understand that if I am checking these boxes and I am not actually following what they are saying, I will be removed from this repository!
SebBanks commented 6 months ago

I also replicated this issue on: Motorolla One 5G Ace - Android 10

aritchie commented 6 months ago

https://github.com/vchelaru/BleBug/blob/117416ce9cb8d5ec22344757fa8b24ba84c92975/BluetoothMauiTest1/MauiProgram.cs#L11

You're building the service container twice so the event is responding to the proper instance.

vchelaru commented 6 months ago

I'm sorry can you clarify? The line that you linked only builds the service provider if it has not yet been built.

serviceProvider = serviceProvider ?? serviceCollection.BuildServiceProvider();

It is equivalent to:

if(serviceProvider == null)
{
    serviceProvider = serviceCollection.BuildServiceProvider();
}

As far as I can tell, this prevents BuildServiceProvider from being built multiple times. Or are you saying that my call to BuildServiceProvider is redundant, and that something else is calling BuildServiceProvider outside of my code?

aritchie commented 6 months ago

https://github.com/vchelaru/BleBug/blob/117416ce9cb8d5ec22344757fa8b24ba84c92975/BluetoothMauiTest1/MauiProgram.cs#L40 - builds container once and sets instance against MAUI events from .UseShiny()

You store the service collection, not the service provider, so once the static is hit, it builds the container again which is a different instance of the blemanager.

You should never be calling BuildServiceProvider yourself.

vchelaru commented 6 months ago

Thank you for the clarification. For future readers who may not be familiar, I was ablet to access the IServiceProvider by modifying my App's constructor so that it takes an IServiceProvider as a parameter:

public App(IServiceProvider serviceProvider)
{
   // Use serviceProvider here if you want to do your own dependency injection and need to access IBleManager
   ...
aritchie commented 6 months ago

Thank you for the clarification. For future readers who may not be familiar, I was ablet to access the IServiceProvider by modifying my App's constructor so that it takes an IServiceProvider as a parameter

I appreciate that you're trying to help others, but unfortunately - this is not a great practice dependency injection. Register your viewmodels as transients in your service provider and inject something like IBleManager into them