QtDataSync  4.2.0
A simple offline-first synchronisation framework, to synchronize data of Qt applications between devices
Adding background synchronization

Table of Contents

A Guide for adding background synchronization to mobile applications

This page consists of two guides, one for android and one for iOs, to add background synchronization to an application. There are multiple steps required that differ for each platform and those are discussed here.

Android Background synchronization

For Android, you will have to add an additional service to your application that is to be run in the background, independently from your main application, to perform the sync. This library already supports all the elements needed to do so, but you still need to setup the project yourself.

You can find a working example of this tutorial in the repository. Check out the AndroidSync Sample.

Note
For the Android variant, you will have to install QtService as well.
See also
QtDataSync::AndroidBackgroundService, QtDataSync::AndroidSyncControl

Prepare the project

Note
For this section it is assumed you are using QtCreator and a qmake based project. For anything else you will have to figure out yourself how to do steps equivalent to these for your build system.

First create any normal project in QtCreator. Select an android kit to build it and then go to "Projects > Build > Build Android APK" and under "Android" press "Create Templates". This should bring up a dialog to add android files to your project. Follow that wizard to add the files. Your pro file should now contain ANDROID_PACKAGE_SOURCE_DIR.

Next you have to add the correct Qt modules. These are: QT += datasyncandroid. Build and run your project. It should still function normally

Add the synchronization service to C++

The next step is to add the actual service to your project that performs the sync. This is done in 3 steps:

Implement the service interface

Add a new C++ class to the project and let it inherit from QtDataSync::AndroidBackgroundService. Adjust the constructor to match the one of the parent class (beware the reference) and implement any methods you need to. The only required method to be implemented is QtDataSync::AndroidBackgroundService::createForegroundNotification. An example would be:

class SyncService : public QtDataSync::AndroidBackgroundService
{
Q_OBJECT
public:
explicit SyncService(int &argc, char **argv);
protected:
void prepareSetup(QtDataSync::Setup &setup) override;
QAndroidJniObject createForegroundNotification() override;
};

And the corresponding implementation:

SyncService::SyncService(int &argc, char **argv) :
AndroidBackgroundService{argc, argv}
{}
void SyncService::prepareSetup(QtDataSync::Setup &setup)
{
// prepare the setup here, i.e.
// setup.setRemoteConfiguration(QtDataSync::RemoteConfig{ ... ]);
}
void SyncService::onSyncCompleted(QtDataSync::SyncManager::SyncState state)
{
qDebug() << "[[SYNCSERVICE]]" << "Sync completed in state:" << state;
exitAfterSync();
}
Note
The foreground notification is needed as an ongoing notification to be shown while the service is running. This is required by Android. For a basic example of how to create the notification see the QtDataSync::AndroidBackgroundService::createForegroundNotification documentation.

Prepare the main to run the service

After creating the sync service class, you need to add code to your main to differentiate between whether the application is started normally or as a service.

The general idea is to split your main into 3 functions. One named activityMain which does the stuff you did before, i.e. create a gui, an serviceMain that launces the service and the actual main that now only decides which of the other two to call based on the start parameters.

The serviceMain should simply instanciate and execute the service from the given parameters:

int serviceMain(int argc, char *argv[])
{
SyncService service{argc, argv};
return service.exec();
}

The actual main has to scan the passed arguments for the --backend argument. If found, the serviceMain should be run, otherwise the activityMain:

int main(int argc, char *argv[])
{
for(auto i = 0; i < argc; i++) {
if(qstrcmp(argv[i], "--backend") == 0)
return serviceMain(argc, argv);
}
return activityMain(argc, argv);
}

Again, the activityMain as basically what was previously the normal main, just moved out into a different function.

Note
It is also possible to create two seperate binaries and reference them. But for the sake of simplicity, only the simple method with one binary is shown here.

Update the AndroidManifest.xml

After the C++ code is now ready, the final step is to update the AndroidManifest.xml. We need to add a few elements to register all components and permissions that are needed to actually run the service.

Add the service

Now that the service was created and is startable if the correct command line arguments are specified, we have to add it to the manifest to make it known to the android system so it can actually be started. The <service> should be placed next to the <activity> element in the manifest, as a child of the root <application> element. The following shows an example for such a service definition:

<service android:process=":service_process" android:name="de.skycoder42.qtservice.AndroidService">
<!-- Application to launch -->
<meta-data android:name="android.app.arguments" android:value="--backend android"/>
<meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
<!-- Ministro -->
<meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
<meta-data android:name="android.app.repository" android:value="default"/>
<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
<meta-data android:name="android.app.bundled_libs_resource_id" android:resource="@array/bundled_libs"/>
<!-- Deploy Qt libs as part of package -->
<meta-data android:name="android.app.bundle_local_qt_libs" android:value="-- %%BUNDLE_LOCAL_QT_LIBS%% --"/>
<meta-data android:name="android.app.bundled_in_lib_resource_id" android:resource="@array/bundled_in_lib"/>
<meta-data android:name="android.app.bundled_in_assets_resource_id" android:resource="@array/bundled_in_assets"/>
<!-- Run with local libs -->
<meta-data android:name="android.app.use_local_qt_libs" android:value="-- %%USE_LOCAL_QT_LIBS%% --"/>
<meta-data android:name="android.app.libs_prefix" android:value="/data/local/tmp/qt/"/>
<meta-data android:name="android.app.load_local_libs" android:value="-- %%INSERT_LOCAL_LIBS%% --"/>
<meta-data android:name="android.app.load_local_jars" android:value="-- %%INSERT_LOCAL_JARS%% --"/>
<meta-data android:name="android.app.static_init_classes" android:value="-- %%INSERT_INIT_CLASSES%% --"/>
<!-- Messages maps -->
<meta-data android:value="@string/ministro_not_found_msg" android:name="android.app.ministro_not_found_msg"/>
<meta-data android:value="@string/ministro_needed_msg" android:name="android.app.ministro_needed_msg"/>
<meta-data android:value="@string/fatal_error_msg" android:name="android.app.fatal_error_msg"/>
<!-- Background running -->
<meta-data android:name="android.app.background_running" android:value="true"/>
</service>

Most of the service was just copied from the <activity> element. The attributes and elements that are different from the activity and thus must be changed after copying it over are:

Add the startup receiver

Now that the service has been added, we need one more component to be added to the manifest. This is the boot receiver, which is started by android after a reboot and is needed to reactive your service after a boot. The class is ready to use, so you only need to add the following to the manifest, right after the <service> element (still inside the <application> element):

<receiver android:name="de.skycoder42.qtdatasync.SyncBootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>

Add additional permissions

The last thing to add to the manifest are additional permissions. We need permissions to start a foreground service (which is exactly what we are doing) and allow the app to receive a signal when the system was rebooted, which is needed to reactive the service after a reboot. Simply add the following elements after the %INSERT_PERMISSIONS comment

<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

Use the controller to enable sync

With all the previous steps the service is fully prepared to be used. All thats left to do is to actually use it by enabeling background synchronization. You can do this with the QtDataSync::AndroidSyncControl class. In the example, it is used from QML you you can play around with the possibilities, but for the sake of this guide, the simplest way is to just statically enable it from your activityMain:

int activityMain(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
// control.setInterval(...);
control.setEnabled(true);
qDebug() << "control.enabled" << control.isEnabled();
//...
}
Attention
Do not forget to register the notification channel for the foreground notification used by the service from your activity (java code below):
public static void registerNotificationChannel(Context context) {
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
return;
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
//create foreground channel
NotificationChannel foreground = new NotificationChannel(ForegroundChannelId,
"Synchronization Service",
NotificationManager.IMPORTANCE_MIN);
foreground.setDescription("Is shown when the application synchronizes");
foreground.enableLights(false);
foreground.enableVibration(false);
foreground.setShowBadge(false);
manager.createNotificationChannel(foreground);
}

Test the setup

With that the preperations are done and ready to test. Compile and run your application and check if you can find control.enabled true in the log. You can also run

adb shell dumpsys alarm | grep -B 1 -A 3 de.skycoder42.qtdatasync.backgroundsync

to check if the alarm was correctly registered and when synchronization is triggered the next time. Wait for the timeout to arrive and see if the service executes correctly (i.e. without crashing). You can consult the logs via

adb logcat

to check the output of the service. You should find a message like:

[[SYNCSERVICE]] Sync completed in state: <some state>

If thats the case, the service works correctly, and you can extend it as you please.

Ios Background synchronization

For Ios, adding background sync requires the registration of a custom delegate that can start the synchronization as part of a background fetch operation. This is done as part of your main application and will run on the main thread - however only when the application is suspended in the background and no gui is currently shown.

You can find a working example of this tutorial in the repository. Check out the IosSync Sample.

Attention
Background fetch on Ios is badly implemented (by apple). You get no guarantee at all that your task will ever be run - if your unlucky, background synchronization will never happen. And since the logic behind that is proprietary apple stuff, there is nothing that can be done to improve the situation. Do not rely on this working for every user, as I can guarantee you it wont.

Prepare the project

Note
For this section it is assumed you are using QtCreator and a qmake based project. For anything else you will have to figure out yourself how to do steps equivalent to these for your build system.

First create any normal project in QtCreator and make shure you select ios or ios simulator as the kit. Next you have to add the correct Qt modules. These are: QT += datasyncios. Build and run your project. It should still function normally

Add the delegate to the project

All you need to do to use this feature is to add a delegate. A default implemented delegate is already part of this library and can be used as is. However, if you want to do something with the new data after it was synchronized, you have to extend the delegate and add custom code.

If thats not relevant for your project, you can skip the next section and immediatly go to Initialize and enable the delegate

Extend the delegate (optional)

If you want to perform custom operations and synchronized data, you have to extend the QtDataSync::IosSyncDelegate. While there are multiple things you can customize, the only relevant method that needs to be reimplemented is QtDataSync::IosSyncDelegate::onSyncCompleted. An example for a custom delegate would be:

class SyncDelegate : public QtDataSync::IosSyncDelegate
{
Q_OBJECT
public:
explicit SyncDelegate(QObject *parent = nullptr);
protected:
};

And the corresponding implementation:

{
qInfo() << "Finished synchronization in state:" << state;
return IosSyncDelegate::onSyncCompleted(state);
}

Initialize and enable the delegate

Once you have your delegate ready (either by using a custom or the default one), the last step in C++ is to register it. This is done via the QtDataSync::IosSyncDelegate::init method from withing the main function:

int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
QtDataSync::IosSyncDelegate::init(new SyncDelegate{&app});
// ...

With that the delegate is automatically registerd and enabled in the system on every start of your application.

Add the Info.plist

The last thing that needs to be done is to declare that your application will use background fetch for the synchronization in the Info.plist file. If you do not already have such a file, simply copy it from your Qt installation. The path where you can find it is currently <path_to_qt_version>/ios/mkspecs/macx-ios-clang/Info.plist.app.

There is only a single key-value pair that you need to add to the root <dict> element at the very bottom:

<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
</array>

A full example for the final Info.plist would be:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleGetInfoString</key>
<string>Created by Qt/QMake</string>
<key>CFBundleIconFile</key>
<string>${ASSETCATALOG_COMPILER_APPICON_NAME}</string>
<key>CFBundleIdentifier</key>
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
<key>CFBundleName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>${QMAKE_SHORT_VERSION}</string>
<key>CFBundleSignature</key>
<string>${QMAKE_PKGINFO_TYPEINFO}</string>
<key>CFBundleVersion</key>
<string>${QMAKE_FULL_VERSION}</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MinimumOSVersion</key>
<string>${IPHONEOS_DEPLOYMENT_TARGET}</string>
<key>NOTE</key>
<string>This file was generated by Qt/QMake.</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<!--! [plist_info_fetch] -->
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
</array>
<!--! [plist_info_fetch] -->
</dict>
</plist>

Test the setup

Testing this on ios is rather complicated, which is why I won't document this here. A helpful articel I found that explains how to test background fetching is linked below:

https://medium.com/livefront/how-to-debug-background-fetch-events-on-ios-29540b043adf