简体   繁体   中英

Using 'Background location services' on iOS with gluon mobile

I just started using gluon mobile and I am developing a small iOS app. I managed to use the PositionService to update the users position on a label in the UI. Now I would like to get Location Updates even if the app is in background mode. Due to apple developers documentation this should work by adding the following key to the apps plist

<key>UIBackgroundModes</key>
<array>
    <string>location</string>
</array>

When deploying the app on an iPhone I can see the updates as long as the App is active. When going to the home screen, the updating stops and in the terminal (gradle -> launchIOSDevice) the message "Stop updating location" is shown. Any idea why I don't get Location Updates in background mode?

Here the relevant code:

    Services.get(PositionService.class).ifPresent(service -> {
        service.positionProperty().addListener((obs, oldPos, newPos) -> {
            posLbl.setText(String.format(" %.7f %.7f\nLast update: " + LocalDateTime.now().toString(),
                    newPos.getLatitude(), newPos.getLongitude()));
            handleData();
        });
    });

Here is the relevant plist entry:

<key>NSLocationAlwaysUsageDescription</key>
<string>A good reason.</string>
<key>UIBackgroundModes</key>
<array>
    <string>location</string>
</array>
<key>NSLocationWhenInUseUsageDescription</key>
<string>An even better reason.</string>

The reason why the Position service is not working when the app goes into background mode can be found here :

Services.get(LifecycleService.class).ifPresent(l -> {
        l.addListener(LifecycleEvent.PAUSE, IOSPositionService::stopObserver);
        l.addListener(LifecycleEvent.RESUME, IOSPositionService::startObserver);
    });
startObserver();

The lifecycle service is designed to prevent doing unnecessary stuff when the app is in background mainly to save battery. Many services, including Position or Battery, make use of it by default.

So far, there is no easy way to remove the listener, as there is no API to enable or disable its use. If you think this should be added, you can file an issue here .

You could fork the Charm Down repository, removing the related code, and build it again, using your own snapshot, but of course this is not a good long term solution.

For now, the only way I can think of, without modifying Down, is avoiding the inclusion of the Lifecycle service implementation for iOS.

Doing so, once you open the app and instantiate the Position service, startObserver will be called and never stopped (until you close the app).

In your build.gradle file, instead of using the downConfig block to include the position plugin, do it in the dependencies block, and remove the traversal dependency to lifecycle-ios:

dependencies {
    compile 'com.gluonhq:charm:4.3.7'
    compile 'com.gluonhq:charm-down-plugin-position:3.6.0'
    iosRuntime('com.gluonhq:charm-down-plugin-position-ios:3.6.0') {
        exclude group: 'com.gluonhq', module: 'charm-down-plugin-lifecycle-ios'
    }
}

jfxmobile {
    downConfig {
        version = '3.6.0'
        plugins 'display', 'statusbar', 'storage'
    }

Now deploy it to your iOS device and check if the position service works on background mode.

EDIT

As pointed out, removing the lifecycle listener that stopped the observer is not enough: the location is not updated in background mode.

The solution to get this working requires modifying the Position service for iOS, and building a local snapshot.

These are the steps (for Mac only):

1. Clone/fork Charm Down

Charm Down is an open source library that can be found here .

2. Edit the Position Service for iOS

We need to comment out or remove the Lifecycle listeners from IOSPositionService ( link ):

public IOSPositionService() {
    position = new ReadOnlyObjectWrapper<>();

    startObserver();
}

(though a better approach will be adding API to allow background mode, and install the listeners based on it. Also a way to stop the observer should be required)

And now we have to modify the native implementation at Position.m ( link ):

- (void)startObserver 
{
    ...
    if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0)
    {
        // try to save battery by using GPS only when app is used:
        [self.locationManager requestWhenInUseAuthorization];
    }

    if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 9.0)
    {
        // allow background mode
        self.locationManager.allowsBackgroundLocationUpdates = YES;
    }
    NSLog(@"Start updating location");
    ...
}

(again, this should be set based on a background mode API)

3. Build and install

At the root of Charm Down, and using a Mac, run this:

./gradlew clean install

(if the Android sdk is not installed, the android services can be commented out at settings.gradle ).

This will install a snapshot of the Charm Down services (currently 3.7.0-SNAPSHOT).

4. Update the Gluon Mobile project

Edit the build.gradle file, and set the mavenLocal() repository, and the snapshot version:

repositories {
    mavenLocal()
    jcenter()
    maven {
        url 'http://nexus.gluonhq.com/nexus/content/repositories/releases'
    }
}

dependencies {
    compile 'com.gluonhq:charm:4.3.7'
}

jfxmobile {
    downConfig {
        version = '3.7.0-SNAPSHOT'
        plugins 'display', 'lifecycle', 'position', 'statusbar', 'storage'
    }

Save and reload the project.

5. Use Position service in background mode

As pointed out in the comments, when running on background mode, iOS doesn't allow making changes in the UI.

This means that whenever a new position is retrieved from the service, we'll have to use a buffer to store it and only when user resumes the app, we'll make the necessary update to the UI with all those buffered locations.

This is a simple use case: with the Lifecycle service we know if we are on foreground or background, and we only update the ListView control (UI) when the app is running on foreground, or it just resumed from background.

private final BooleanProperty foreground = new SimpleBooleanProperty(true);

private final Map<String, String> map = new LinkedHashMap<>();
private final ObservableList<String> positions = FXCollections.observableArrayList();

public BasicView(String name) {
    super(name);

    Services.get(LifecycleService.class).ifPresent(l -> {
        l.addListener(LifecycleEvent.PAUSE, () -> foreground.set(false));
        l.addListener(LifecycleEvent.RESUME, () -> foreground.set(true));
    });
    ListView<String> listView = new ListView<>(positions);

    Button button = new Button("Start GPS");
    button.setGraphic(new Icon(MaterialDesignIcon.GPS_FIXED));
    button.setOnAction(e -> {
        Services.get(PositionService.class).ifPresent(service -> {
            foreground.addListener((obs, ov, nv) -> {
                if (nv) {
                    positions.addAll(map.values());
                }
            });
            service.positionProperty().addListener((obs, oldPos, newPos) -> {
                if (foreground.get()) {
                    positions.add(addPosition(newPos));
                } else {
                    map.put(LocalDateTime.now().toString(), addPosition(newPos));
                }
            });
        });
    });

    VBox controls = new VBox(15.0, button, listView);
    controls.setAlignment(Pos.CENTER);

    setCenter(controls);
}

private String addPosition(Position position) {
    return LocalDateTime.now().toString() + " :: " + 
            String.format("%.7f, %.7f", position.getLatitude(), position.getLongitude()) +
            " :: " + (foreground.get() ? "F" : "B");
}

Finally, as pointed out by the OP, make sure to add the required keys to the plist:

<key>NSLocationAlwaysUsageDescription</key>
<string>A good reason.</string>
<key>UIBackgroundModes</key>
<array>
    <string>location</string>
</array>
<key>NSLocationWhenInUseUsageDescription</key>
<string>An even better reason.</string>

6. Deploy and run

Allow location use, start the location service, and when entering background mode, a blue status bar on the iOS device will show that the App is using location.

Note that this could drain the battery really fast .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM