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.
Related
I have tried getting the position service to work. i do not get the pop up like android notifying me to allow location tracking.
i have updated my plist based on solutions online see below.
label1.setText("Waiting for GPS signal");
Services.get(PositionService.class).ifPresent(service -> {
// if there is GPS service, disable button to avoid adding more
// listeners
// label1.setText("Waiting for GPS signal");
service.positionProperty().addListener((obs, ov, nv)
-> label1.setText(+nv.getLatitude() + ", " + nv.getLongitude()));
});
find the updated plist file
link
everything build fine just the location not working what could be wrong
I'm attempting to configure AppCenter.Distribute for in-app updates within my Xamarin Android app. Here is the very basic setup code, which I have in my main launcher activity's OnCreate method (AFTER the base.OnCreate call):
AppCenter.Start (Resources.GetString (Resource.String.appcenter_app_secret), typeof (Analytics), typeof (Crashes), typeof (Distribute));
I was able to get the in-app updates to supposedly initialize. When I first install and open the app, it shows a browser window for one second that says "In-app updates enabled! Returning to app in 1...", then it redirects back to my app. Unfortunately, when I then bump the version name and code and distribute a new build, I don't get a dialog within the app prompting me to update to the new version.
I even tried handling the Distribute.ReleaseAvailable action and showing a custom dialog, and that action isn't invoked either:
Distribute.ReleaseAvailable = OnReleaseAvailable;// Called before AppCenter.Start
private bool OnReleaseAvailable(ReleaseDetails releaseDetails)
{
// Show custom dialog.
Droid.ApplicationContext.Activity.CustomDialogBuilder().Show(new NotificationArgs
{
Title = "New update available!",
Message = "A new version of RPR Mobile, {0} ({1}) is available. Release notes: {2}"
.WithFormat(releaseDetails.ShortVersion, releaseDetails.Version, releaseDetails.ReleaseNotes),
PositiveButtonText = "Update",
PositiveAction = () =>
{
// Notify SDK that user selected to update...
Distribute.NotifyUpdateAction(UpdateAction.Update);
},
HideNegativeButton = releaseDetails.MandatoryUpdate,
NegativeButtonText = "Postpone Update",
NegativeAction = () =>
{
// Notify SDK that user selected to postpone (for 1 day)...
// Note that this method call is ignored by the SDK if the update is mandatory.
Distribute.NotifyUpdateAction(UpdateAction.Postpone);
}
});
// Return true if you are using your own dialog, false otherwise.
return true;
}
I'm wondering what I'm missing. Some questions that may or may not be relevant...
Does it matter whether the AppCenter.Start code executes before or after the base.OnCreate call?
Does it matter whether the activity that AppCenter.Start is called from is running or finished? Because in our case, the main launcher is just a splash screen that closes after a couple seconds.
Is the App Center SDK supposed to poll every few seconds for an update? Or does it check only when opening and closing activities?
It turns out that you have to close and relaunch your app for it to check for new updates. The documentation could be more clear on this...
I'm actually working on a Xamarin Forms application that need push notifications. I use the Plugin.PushNotification plugin.
When the app is running in the foreground or is sleeping (OnSleep), I have no problem to open a specific page when I click on a notification that I receive. But I was wondering how can I do that when the app is closed. Thanks!
I finally found the answer by myself and I want to share it in case someone needs it.
Nota bene: according to the official documentation of the plugin, it's Xam.Plugin.PushNotification that is deprecated. I use the new version of this plugin, Plugin.PushNotification which uses FCM for Android and APS for iOS.
There is no significant differences to open a notif when the app is running, is sleeping or is closed. Just add the next callback method in the OnCreate method (MyProject.Droid > MainApplication > OnCreate) and FinishedLaunching method (MyProject.iOS > AppDelegate > FinishedLaunching):
CrossPushNotification.Current.OnNotificationOpened += (s, p) =>
{
// manage your notification here with p.Data
App.NotifManager.ManageNotif(p.Data);
};
Common part
App.xaml.cs
// Static fields
// *************************************
public static NotifManager NotifManager;
// Constructor
// *************************************
public App()
{
...
NotifManager = new NotifManager();
...
}
NotifManager.cs
public class NotifManager
{
// Methods
// *************************************
public void ManageNotif(IDictionary<string, object> data)
{
// 1) switch between the different data[key] you have in your project and parse the data you need
// 2) pass data to the view with a MessagingCenter or an event
}
}
Unfortunately there is no succinct answer for either platform. Generally speaking, you need to tell the OS what to do when it starts the app as a result of the push notification. On both platforms, you should also consider what API level you are targeting, otherwise it won't work or even crash the app.
On iOS, you will need to implement this method in AppDelegate appropriately: FinishedLaunching(UIApplication application, NSDictionary launchOptions). The launchOptions will have the payload from the push notification for you to determine what to do with it (e.g. what page to open). For more information on iOS, Xamarin's documentation is a good place to start.
Android has a more complicated topology in terms of more drastic differences between API levels, whether you are using GCM/FCM, as well as requiring more code components. However, to answer the question directly, you will need to handle this in OnCreate(Bundle savedInstanceState) of your main Activity. If you are using Firebase, the push notification payload is available in Intent.Extras. Again, Xamarin's documentation has a good walkthrough.
Finally, note that the Plugin.PushNotification library you are using has been deprecated. I suggest you either change your library and/or your implementation soon. Part of the reason that library has been deprecated is because Google has deprecated the underlying Google Cloud Messaging (GCM) service, which will be decommissioned on April 11, 2019.
I created a new Xamarin.Forms app to test basic Bluetooth functionality. I downloaded this plugin into both the Android project and the shared project:
https://github.com/aritchie/bluetoothle
I wrote this function in the shared project and am calling it from the OnCreate of my launch activity in my Android project:
public static async Task BroadcastBluetooth()
{
// (I do not await this function when I call it)
try
{
await Task.Delay(5000); // just to make sure we give enough time for all initialization to complete
_server = CrossBleAdapter.Current.CreateGattServer();
// exception thrown on this line
await _server.Start(new AdvertisementData
{
LocalName = "TestServer",
ServiceUuids = new List<Guid>()
});
}
catch (Exception e)
{
}
}
It throws this exception:
{System.NullReferenceException: Object reference not set to an
instance of an object. at
Plugin.BluetoothLE.Server.GattServer.StartAdvertising
(Plugin.BluetoothLE.Server.AdvertisementData adData) [0x00095] in
C:\dev\acr\bluetoothle\Plugin.BluetoothLE.Android\Server\GattServer.cs:135
at Plugin.BluetoothLE.Server.GattServer.Start
(Plugin.BluetoothLE.Server.AdvertisementData adData) [0x00011] in
C:\dev\acr\bluetoothle\Plugin.BluetoothLE.Android\Server\GattServer.cs:70
at App2.App+d__7.MoveNext () [0x00097] in
C:\Projects\app2\App2\App2\App.xaml.cs:49 }
I'm only doing something really basic so I must be missing something? The exception is referencing a directory path of the plugin's developer's machine (C:\dev\acr...) so either this plugin is broken or I'm doing something really wrong?
I am using the same plugin in a current project of mine and it is working fine. So I doubt the issue is within the plugin.
Some thoughts about what could cause the issue:
Are you testing this code on a real device that is capable of performing BLE Advertisement?
Do you have set the permissions accordingly in your android project?
Does BLE Advertising work when you use the native android apis?
It also would be helpful, if you could attach a repository with which I can reproduce the issue.
I am using the pro library.
But I just found doc for free library
I cannot find any doc for pro version.
Also, I don't know how to implement the background mode even using the pro sample.
Here are the steps:
Build the pro sample project
start the iBeacon source(using iPad) and it can be detected
start the application and then press home button the make it in
background
Turn off the iBeacon source
Turn on the iBeacon source
However, more than 5 minutes, the application does not launch
So, can anyone verify the step I did?
How can I test the background mode more easily?
Also, for the BootstrapNotifier, is it just work only first time when the device reboot?
After that, even I put application in background, the application will not launch when it detect iBeacon?
Your testing method sounds fine. I think the issue is that the reference app for the pro library only auto launches the app on the first detection after boot. After that, it sends a notification instead, and tapping on that notification launches the app.
This is purely for demonstration purposes. You can change it to auto launch on every detection if you wish. Simply alter the haveDetectedIBeaconsSinceBoot logic in this code:
#Override
public void didEnterRegion(Region arg0) {
// In this example, this class sends a notification to the user whenever an iBeacon
// matching a Region (defined above) are first seen.
Log.d(TAG, "did enter region.");
if (!haveDetectedIBeaconsSinceBoot) {
Log.d(TAG, "auto launching MainActivity");
// The very first time since boot that we detect an iBeacon, we launch the
// MainActivity
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// Important: make sure to add android:launchMode="singleInstance" in the manifest
// to keep multiple copies of this activity from getting created if the user has
// already manually launched the app.
this.startActivity(intent);
haveDetectedIBeaconsSinceBoot = true;
} else {
// If we have already seen iBeacons and launched the MainActivity before, we simply
// send a notification to the user on subsequent detections.
Log.d(TAG, "Sending notification.");
sendNotification();
}
}
The javadoc link was missing from the main documentation page when you posted this question. That is fixed now.