[GUIDE][DEPRECATED] How to bundle RS service APK and execute it as foreground service
Deprecated
In favour of #60 (closed)
TL:DR;
This is a guide that will describe how to bundle a RetroShare service APK into a Flutter app, execute it as foreground, and stop/start the service.
We need to run the RetroShare service in a separated thread as an Android foreground service. The foreground services need a notification that will maintain the process alive in any case. Android background service went to an idle status after a while, and also the normal apps that are not focused.
The foreground services don't do that, for example a music player notification or TelegramFOSS notification that let recieve messages while the app is not focused.
Previous
-
Install QT for Android version 5.12.2. You will need other dependencies like Java or Android SDK tools. After that, using Qt Maintenance tool, you have to download Qt Android pre-built components for Android-ARM64-v8aandAndroid-ARMv7. -
Flutter Android embedding v2project is needed.
1. Configure retroshare-service.properties
First of all you need the armeabi-v7 and arm64-v8a APK builds. For the moment is not a distribution place for that, and compilation is not easy at all, ask here for more information.
When you get the APK's, create a retroshare-service.properties file on android/ directory inside the flutter app and fill it like that:
cd your/flutter/app && cd android/
touch retroshare-service.properties
# open it with your favorite editor and edit the following
// Qt for Android install path
qt.installdir=/home/user/Qt/5.12.5
// Paths of retroshare-service APKs separated by ':' to extract the native libraries
retroshare.servicepackages=/<RS-service-path>/android-build-debug-armeabi-v7a.apk:/<RS-service-path>/android-build-debug-arm64-v8a.apk
2. Set up Gradle instructions.
Hope that I'm not forgetting nothing. Edit <Flutter project>/android/app/build.gradle. Add the following after definition of flutter properties:
[...]
def rsProp = new Properties()
def rsPropFileName = 'retroshare-service.properties'
def rsPropFile = rootProject.file(rsPropFileName)
if(rsPropFile.exists())
{
rsPropFile.withReader('UTF-8') { reader ->
rsProp.load(reader)
}
}
else
{
throw new FileNotFoundException('File '+rsPropFileName+' not found!')
}
rsProp.getRequiredProperty = { propName ->
def retval = rsProp.getProperty(propName)
if(retval == null)
{
throw new FileNotFoundException(
'Property: '+propName+' not found in: '+rsPropFileName+' !' )
}
return retval
}
def qtAndroidDirPropName = 'qt.installdir'
def qtAndroidDir = new File(rsProp.getRequiredProperty(qtAndroidDirPropName))
if(qtAndroidDir.isDirectory())
{
def mCandDirs = []
qtAndroidDir.eachDir() {
if(it.name.startsWith('android')) { mCandDirs << it.name }
}
if(mCandDirs.size() < 1)
{
throw new FileNotFoundException(
qtAndroidDirPropName + ' in: ' + rsPropFileName + " doesn't point \
to a valid Qt for Androd installation directory" )
}
qtAndroidDir = qtAndroidDir.getCanonicalPath() + '/' + mCandDirs[0]
}
else
{
throw new FileNotFoundException(
qtAndroidDirPropName + ' in: ' + rsPropFileName + " must point to a valid \
directory" )
}
def rsServiceBlobDir = new File(rootProject.buildDir, 'retroshare-service')
if(!rsServiceBlobDir.exists()) rsServiceBlobDir.mkdirs()
[...]
Then modify the SourceSets property as shown here:
[...]
copy {
rsProp.getRequiredProperty('retroshare.servicepackages').split(':').each {
from zipTree(it) into rsServiceBlobDir
}
}
sourceSets {
main {
// manifest.srcFile = [ 'src/main/AndroidManifest.xml',
// 'libs/AndroidManifest.xml' ]
java.srcDirs = [ qtAndroidDir + '/src/android/java/src',
'src/main/kotlin', 'src/main/java' ]
aidl.srcDirs = [ qtAndroidDir + '/src/android/java/src', 'src',
'aidl' ]
res.srcDirs = [ qtAndroidDir + '/src/android/java/res', 'res',
'src/main/res' ]
resources.srcDirs = ['src']
renderscript.srcDirs = ['src']
assets.srcDirs = [ 'assets',
rsServiceBlobDir.getCanonicalPath() + '/assets' ]
jniLibs.srcDirs += rsServiceBlobDir.getCanonicalPath() + '/lib'
}
}
[...]
Finally add inside dependencies:
[...]
dependencies {
[...]
implementation fileTree(dir: qtAndroidDir +'/jar/', include: ['*.jar'])
}
I think its all. Original file.
3. Add RS service to AndroidManifest.xml
Open and edit <Flutter project>/android/app/src/main/AndroidManifest.xml and add the following code into the <application> tag (font). Pay attention to android:name=".RetroShareServiceAndroid" where RetroShareServiceAndroid will be the name of the Android class that will execute the service (on the next step).
<!-- For adding service(s) please check:
++ https://wiki.qt.io/AndroidServices -->
<service android:name=".RetroShareServiceAndroid" android:label="RetroShare Service" android:process=":rs"> <!-- android:exported="true" android:process=":rs" -->
<!-- android:exported="true" must be added to be able to run the service
++ from adb shell
++ android:process=":rs" is needed to force the service to run on
++ a separate process than the Activity -->
<!-- Qt Application to launch -->
<meta-data android:name="android.app.lib_name" android:value="retroshare-service"/>
<!-- 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="1"/>
<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="1"/>
<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="plugins/platforms/android/libqtforandroid.so:plugins/bearer/libqandroidbearer.so"/>
<meta-data android:name="android.app.load_local_jars" android:value="jar/QtAndroid.jar:jar/QtAndroidExtras.jar:jar/QtAndroidBearer.jar"/>
<meta-data android:name="android.app.static_init_classes" android:value=""/>
<!-- 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"/>
<!-- Messages maps -->
<!-- Background running -->
<meta-data android:name="android.app.background_running" android:value="true"/>
<!-- Background running -->
</service>
This will attach the RS service to your Android app. Pay attention to the option android:process=":rs" that make RetroShare running as separated service, what achieve us tu stop/start it from the flutter app without killing our app.
Also add the permisions to execute it as foreground service. We will need the REQUEST_IGNORE_BATTERY_OPTIMIZATIONS to prevent killing the service from Android panel on some devices, see.
<!-- Add to mantain RS service up -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
At this point, the RS Apk should be bundled inside your flutter app. Now lets see how to start and stop it from the Flutter app.
4. Add Android code to start/stop the service
We used Kotlin for this example. So, copy RetroShareServiceAndroid.kt on <Flutter project>/android/app/src/main/kotlin/<your>/<project>/<domain>/RetroShareServiceAndroid.kt paying attention to adapt the code to your project, such as imports.
To manage that permisions are set properly, we adapted the code from the flutter background plugin. Copy PermissionHandler.kt at the same level of RetroShareServiceAndroid.kt.
After import this code, open your MainActivity.kt and copy the code from our. Pay attention to configureFlutterEngine where the calls to start/stop RS are called:
"hasPermissions" -> {
var hasPermissions = permissionHandler!!.isIgnoringBatteryOptimizations()
&& permissionHandler!!.isWakeLockPermissionGranted()
result.success(hasPermissions)
}
"initialize" -> {
val title = call.argument<String>("android.notificationTitle")
val text = call.argument<String>("android.notificationText")
val importance = call.argument<Int>("android.notificationImportance")
val iconName = call.argument<String>("android.notificationIconName")
val iconDefType = call.argument<String>("android.notificationIconDefType")
// Set static values so the RetroShareServiceAndroid can use them later on to configure the notification
notificationImportance = importance ?: notificationImportance
notificationTitle = title ?: notificationTitle
notificationText = text ?: text
notificationIconName = iconName ?: notificationIconName
notificationIconDefType = iconDefType ?: notificationIconDefType
// Ensure wake lock permissions are granted
if (!permissionHandler!!.isWakeLockPermissionGranted()) {
result.error("PermissionError", "Please add the WAKE_LOCK permission to the AndroidManifest.xml in order to use background_sockets.", "")
}
// Ensure ignoring battery optimizations is enabled
if (!permissionHandler!!.isIgnoringBatteryOptimizations()) {
if (activity != null) {
permissionHandler!!.requestBatteryOptimizationsOff(result, activity!!)
} else {
result.error("NoActivityError", "The plugin is not attached to an activity", "The plugin is not attached to an activity. This is required in order to request battery optimization to be off.")
}
}
result.success(true)
}
"enableBackgroundExecution" -> {
// Ensure all the necessary permissions are granted
if (!permissionHandler!!.isWakeLockPermissionGranted()) {
result.error("PermissionError", "Please add the WAKE_LOCK permission to the AndroidManifest.xml in order to use background_sockets.", "")
} else if (!permissionHandler!!.isIgnoringBatteryOptimizations()) {
result.error("PermissionError", "The battery optimizations are not turned off.", "")
} else {
val intent = Intent(context, RetroShareServiceAndroid::class.java)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
context!!.startForegroundService(intent)
} else {
context!!.startService(intent)
}
result.success(true)
}
}
"disableBackgroundExecution" -> {
val intent = Intent(context!!, RetroShareServiceAndroid::class.java)
intent.action = RetroShareServiceAndroid.ACTION_SHUTDOWN
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
context!!.startForegroundService(intent)
} else {
context!!.startService(intent)
}
result.success(true)
}
else -> {
result.notImplemented()
}
Well, now we should be able to start and stop the service from the Flutter app. And now..
5. Flutter code
We didn't achieve yet to create a Flutter plugin that do all of this for you, but probably we'll do it in the future. For the moment, add the following code to your project.
-
The android_config.dart will manage some of the configurations for the foreground service. But sadly this is not working for the moment, if you need to change something, do it hardcoding the parameters on the android code.
-
On rscontrol.dart you will find the invoked methods from the flutter app to the kotlin code. The most relevant calls are:
static bool _isInitialized = false;
static bool _isBackgroundExecutionEnabled = false;
/// Initializes the plugin.
/// May request the necessary permissions from the user in order to run in the background.
///
/// Does nothing and returns true if the permissions are already granted.
/// Returns true, if the user grants the permissions, otherwise false.
/// May throw a [PlatformException].
static Future<bool> initialize(
{FlutterRetroshareServiceAndroidConfig androidConfig =
const FlutterRetroshareServiceAndroidConfig()}) async {
_isInitialized = await rsPlatform.invokeMethod<bool>('initialize', {
'android.notificationTitle': androidConfig.notificationTitle,
'android.notificationText': androidConfig.notificationText,
'android.notificationImportance': _androidNotificationImportanceToInt(
androidConfig.notificationImportance),
'android.notificationIconName': androidConfig.notificationIcon.name,
'android.notificationIconDefType':
androidConfig.notificationIcon.defType,
}) ==
true;
return _isInitialized;
}
/// Enables the execution of the flutter app in the background.
/// You must to call [RsServiceControl.initialize()] before calling this function.
///
/// Returns true if successful, otherwise false.
/// Throws an [Exception] if the plugin is not initialized by calling [FlutterBackground.initialize()] first.
/// May throw a [PlatformException].
static Future<bool> enableBackgroundExecution() async {
if (_isInitialized) {
final success =
await rsPlatform.invokeMethod<bool>('enableBackgroundExecution');
_isBackgroundExecutionEnabled = true;
return success == true;
} else {
throw Exception(
'RsServiceControl plugin must be initialized before calling enableBackgroundExecution()');
}
}
/// Disables the execution of the flutter app in the background.
/// You must to call [FlutterBackground.initialize()] before calling this function.
///
/// Returns true if successful, otherwise false.
/// Throws an [Exception] if the plugin is not initialized by calling [FlutterBackground.initialize()] first.
/// May throw a [PlatformException].
static Future<bool> disableBackgroundExecution() async {
if (_isInitialized) {
final success =
await rsPlatform.invokeMethod<bool>('disableBackgroundExecution');
_isBackgroundExecutionEnabled = false;
return success == true;
} else {
throw Exception(
'RsServiceControl plugin must be initialized before calling disableBackgroundExecution()');
}
}
/// Indicates whether or not the user has given the necessary permissions in order to run in the background.
///
/// Returns true, if the user has granted the permission, otherwise false.
/// May throw a [PlatformException].
static Future<bool> get hasPermissions async {
return await rsPlatform.invokeMethod<bool>('hasPermissions') == true;
}
/// Indicates whether background execution is currently enabled.
static bool get isBackgroundExecutionEnabled => _isBackgroundExecutionEnabled;
static int _androidNotificationImportanceToInt(
AndroidNotificationImportance importance) {
switch (importance) {
case AndroidNotificationImportance.Low:
return -1;
case AndroidNotificationImportance.Min:
return -2;
case AndroidNotificationImportance.High:
return 1;
case AndroidNotificationImportance.Max:
return 2;
case AndroidNotificationImportance.Default:
default:
return 0;
}
}
Finally, on your app, you have to do something like:
// Initialize RsServiceControl with default values
rsControl.RsServiceControl.initialize().then((value) {
print("RsServiceControl initialize Ok");
await rsControl.RsServiceControl.enableBackgroundExecution();
// Do whatever
});
Update November-22-2021
When release apk is built, some RS classes are dismissed on the shrink process. Add proguard rules to avoid that as shown here: