transistorsoft / capacitor-background-geolocation

The most sophisticated background location-tracking & geofencing module with battery-conscious motion-detection intelligence for iOS and Android.
MIT License
96 stars 16 forks source link

No permission dialog on android. #236

Closed mley closed 8 months ago

mley commented 8 months ago

Your Environment

Expected Behavior

When using location services for the first time after a fresh install, there should be a popup to ask for permissions for location and other required permissions. After permissions are granted, we can access location data.

Actual Behavior

No permission dialog was shown. Accessing location data failed.

Context

We're developing a fitness app to track sports activities like running, biking, etc. We're mainly using "foreground" location tracking with watchPosition (meaning: the app is running all the time, maybe it's in the background, maybe not, screen is off, device is locked). Yes, I've read the philosophy of operation for this plugin. Since the open source cordova and capacitor plugin for location tracking to not really work well for our use-case, we tried your plugin, hoping to improve user experience.

The same code below works fine in iOS.

The logs were take from a debug build.

What maybe of concern: We're also using the cordova-health-plugin, which accesses fitness and activity data. I don't know how much of these permissions overlap with your plugin. The cordova-health-plugin will only work properly with a PlayStore release, because of Google Health.

https://github.com/transistorsoft/capacitor-background-geolocation/assets/7995757/ea77ddf9-5118-44d8-9286-a7d4385f49ae

When "Running" was tabbed, the startLocationTracking method of the LocationService is called.

If you need any more info, please let me know.

Code

import { Injectable, OnDestroy } from '@angular/core';
import { BehaviorSubject, Observable, Subject, switchMap } from 'rxjs';
import { LoggingService } from '../../../services/logging.service';
import BackgroundGeolocation, { Subscription } from '@transistorsoft/capacitor-background-geolocation';
import { fromPromise } from 'rxjs/internal/observable/innerFrom';
import { TranslateService } from '@ngx-translate/core';

export interface GpsLocation {
  latitude: number;
  longitude: number;
  altitude: number;
  bearing: number;
}

@Injectable({
  providedIn: 'root',
})
export class LocationService implements OnDestroy {
  private headingSubject = new BehaviorSubject<number | undefined>(undefined);
  public heading$: Observable<number | undefined> = this.headingSubject.asObservable();

  constructor(
    private readonly loggingService: LoggingService,
    private readonly translateService: TranslateService
  ) {}

  ngOnDestroy(): void {
    this.subscriptions.forEach(s => s.remove());
    this.subscriptions.length = 0;
  }

  /**
   * Start continuous location tracking.
   *
   * @returns Observable of locations
   */
  public startLocationTracking(): Observable<GpsLocation> {
    return fromPromise(this.initialize()).pipe(switchMap(() => this.watchLocation()));
  }

  private subject: Subject<GpsLocation>;

  private watchLocation(): Observable<GpsLocation> {
    if (!!this.subject) {
      this.loggingService.warn('watchLocation already running');
      return this.subject.asObservable();
    }

    this.subject = new Subject<GpsLocation>();
    this.loggingService.log('start watchLocation');
    BackgroundGeolocation.watchPosition(
      location => {
        this.loggingService.log('watchPosition location:', location);
        this.headingSubject.next(location.coords.heading);
        this.subject.next({
          latitude: location.coords.latitude,
          longitude: location.coords.longitude,
          altitude: location.coords.altitude,
          bearing: location.coords.heading,
        });
      },
      error => {
        this.loggingService.error('watchPosition error', error);
        this.subject.error(error);
      },
      {
        interval: 1000,
        desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_HIGH,
        persist: true,
      }
    );

    return this.subject.asObservable();
  }

  /**
   * Stop the continuous location tracking.
   */
  public async stopLocationTracking(): Promise<void> {
    this.loggingService.log('stop watchLocation');
    await BackgroundGeolocation.stopWatchPosition();
    await BackgroundGeolocation.stop();

    this.subject.complete();
    this.subject = undefined;
  }

  private subscriptions: Subscription[] = [];

  private async initialize(): Promise<void> {
    if (this.subscriptions.length > 0) {
      this.subscriptions.push(
        BackgroundGeolocation.onLocation(location => {
          this.loggingService.log('onLocation', location);
        })
      );

      this.subscriptions.push(
        BackgroundGeolocation.onMotionChange(event => {
          this.loggingService.log('onMotionChange', event);
        })
      );

      this.subscriptions.push(
        BackgroundGeolocation.onActivityChange(event => {
          this.loggingService.log('onActivityChange', event);
        })
      );

      this.subscriptions.push(
        BackgroundGeolocation.onProviderChange(event => {
          this.loggingService.log('onProviderChange', event);
        })
      );
    }

    const readyState = await BackgroundGeolocation.ready({
      // Geolocation Config
      desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_HIGH,
      desiredOdometerAccuracy: 10,
      locationAuthorizationRequest: 'WhenInUse',

      //iOS only
      stationaryRadius: 5,
      showsBackgroundLocationIndicator: true,
      activityType: 3, //ACTIVITY_TYPE_FITNESS
      preventSuspend: true,

      //Android only
      allowIdenticalLocations: true,
      foregroundService: true,
      backgroundPermissionRationale: {
        title: this.translateService.instant('LOCATION.ENABLE_LOCATION_TITLE'),
        message: this.translateService.instant('LOCATION.ENABLE_LOCATION_MESSAGE'),
        positiveAction: this.translateService.instant('LOCATION.ALLOW'),
        negativeAction: this.translateService.instant('ENABLE_LOCATION_CANCEL'),
      },
      notification: {
        title: 'Tracking',
        text: 'Tracking your location',
      },
      geofenceModeHighAccuracy: true,

      // Application config
      debug: false, // <-- enable this hear sounds for background-geolocation life-cycle.
      logLevel: BackgroundGeolocation.LOG_LEVEL_VERBOSE,
    });

    this.loggingService.log('ready state', readyState);

    const startState = await BackgroundGeolocation.start();

    this.loggingService.log('start state', startState);
  }
}

Debug logs

https://gist.github.com/mley/97d77729cae8706d54a65d70cf28bd73

mley commented 8 months ago

Update: I've run the app after a fresh install again without using the cordova-health-plugin (which asks for permission for physical activity, as this plugin does). When the background location plugin is started, the app asks for the physical activity permission. I allowed access, but no other permissions are requested.

The TSLocationManager again logs

[c.t.l.u.LocationAuthorization$j onPermissionDenied]
⚠️  LocationAuthorization: Permission denied

The same happens when manually granting all related permissions in the Android settings for my app.

christocracy commented 8 months ago

Avoid using .watchPosition().

The plug-in requests permission when you call .start().

Read the wiki "Philosophy of Operation".

mley commented 8 months ago

I've changed the code to not use watchPosition.

Calling .ready() returns successfully, but calling .start() throws an error, that permissions were denied. No dialog to grant permissions is shown. And that all happened before .getCurrentPosition() is called.

All required permissions are already granted manually:

Screenshot_20240226_160740_Permission controller

We would really like to license this plugin for multiple apps, if we get it working properly.

christocracy commented 8 months ago

Create for me a simple "hello-world" app which reproduces your problem and share it with me in a public GitHub repo so I can test it.

mley commented 8 months ago

Here's a simple project that reproduces the error: https://github.com/mley/bgloc-hello-world

That was a nice exercise finding the dependency that breaks things. It seems the source of the issue is the presence of the cordova-ble-central plugin. If I remove that, it works fine. We need this plugin to connect to BLE heart rate monitors. I need to check the state of the capacitor bluetooth LE plugin to see if I can get rid of the cordova plugin.

Another solution might be using the slim-version of the plugin ( https://github.com/don/cordova-plugin-ble-central?tab=readme-ov-file#android-permission-conflicts ), but I must admit, that I haven't checked the final permissions in the merged manifest yet.

christocracy commented 8 months ago

You shouldn't be using Cordova plugins with capacitor. The ability to do so is only a crutch until you implement a pure Capacitor version.

mley commented 8 months ago

The same error keeps occurring when using the @capacitor-community/bluetooth-le plugin. I've updated the demo project accordingly.

christocracy commented 8 months ago

You can manually call .requestPermission method.

mley commented 8 months ago

That doesn't work either. requestPermission() only returns

{
    "success": false,
    "status": 2
}

There is no permission dialog and start() still fails with missing permissions.

mley commented 8 months ago

I've updated the sample app. Did you get a chance to check it out?

It just takes a few commands to run it on an Android device:

npm install
npx ionic cap run android
mley commented 8 months ago

I did some more testing and found out, that the issue is indeed related to the capacitor bluetooth plugin, but not the plugin itself, but the permissions in the manifest.

As far as I understand up to certain Android version, apps also needed location permissions to use bluetooth, but later version do not need them. The documentation of the BLE plugins therefore suggests to add following permissions:

 <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="30" />
 <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />

It seems the attribute android:maxSdkVersion="30" makes Android ignore the permission if the SDK version if above 30. If I remove the attribute, the location permission dialog comes up and getting location data works.

Thanks for helping out.