Titanium JIRA Archive
Titanium SDK/CLI (TIMOB)

[TIMOB-16066] Android: Add "foreground" service support

GitHub Issuen/a
TypeNew Feature
PriorityHigh
StatusClosed
ResolutionFixed
Resolution Date2018-06-06T01:31:04.000+0000
Affected Version/sn/a
Fix Version/sRelease 7.3.0
ComponentsAndroid
Labelsandroid, background, demo_app, exalture, foreground, services
ReporterDirlei Dionísio
AssigneeJoshua Quick
Created2013-12-21T20:36:49.000+0000
Updated2018-06-19T21:57:02.000+0000

Description

Some tasks must be done in a background services but they are essential to an app (like playing audio in a music player, radio player or audiobook player). The problem is that when we use standard background services, Android can kill the service, resulting in a bad user experience. From the Android documentation: "A started service can use the startForeground(int, Notification) API to put the service in a foreground state, where the system considers it to be something the user is actively aware of and thus not a candidate for killing when low on memory." (1) "By default services are background, meaning that if the system needs to kill them to reclaim more memory (such as to display a large page in a web browser), they can be killed without too much harm. You can set this flag if killing your service would be disruptive to the user, such as if your service is performing background music playback, so the user would notice if their music stopped playing." (2) It will be very appreciated if we can use foreground services in Titanium without the need of writing a native Android module.

[http://developer.android.com/reference/android/app/Service.html#ProcessLifecycle]

[http://developer.android.com/reference/android/app/Service.html#startForeground(int,%20android.app.Notification)]

Comments

  1. Ritu Agrawal 2013-12-22

    Thank you for your valuable suggestion. Titanium SDK currently does not support foreground services so moving this feature request to engineering for further evaluation and prioritization.
  2. Matěj Šimek 2014-01-28

    Some draft of mine: https://github.com/enzy/titanium_mobile/commit/a979da5d5887612d9fc0d297882f7518eb566c5b Currently I don't know how to pass notification (NotificationProxy) to startForeground method.
  3. Karl Nilsson 2014-01-28

    Matěj: I don't think that will work. The services use a java-template that is included in the project by the _build.js script. I created a custom template and added the startForeground() call and successfully managed to start a foreground service. However, as soon as the app is killed (but the service keeps running) the javascript code stops running. After having looked at the source code deeper I came to the conclusion that the Kroll class is static, which means only one copy can be run, and that copy is killed along with the app process itself. So the problem seems to be much much deeper than just calling startForeground() etc. But I am not a java programmer, so I may have misunderstood how the underlying code works.
  4. Ingo Muschenetz 2014-09-09

    Would like to see an architecture proposal first on how to solve this.
  5. greg 2017-03-20

    There is an open source GPS tracker that handles this well on Android. Of course its written in Java, however I wonder if this may assist in working out how Appcelerator could be enhanced to support this functionality. http://bk.gnarf.org/creativity/bigbrothergps/
  6. Carlos Pérez López 2018-01-31

    Hi, something new? I need it for gps tracker in android oreo in background
  7. Joshua Quick 2018-04-23

    Android 8.0 and above automatically throttles location data collection while the app is backgrounded to about 3 times per hour. This is regardless if your app targets Android 8 or not. The only solution is to use a foreground service. https://developer.android.com/about/versions/oreo/background-location-limits.html Android 8.0 (aka: API Level 26) has a new Context.startforegroundservice() method, but I don't know if we need to call it over Context.startService() since you're still required to call Service.startForeground() afterwards within 5 seconds (this is in regards to location data throttling). This requires research and will determine how to implement this in Titanium.
  8. greg 2018-04-23

    Rainer Schleevoigt is doing some work on this which may help... https://github.com/AppWerft/Ti.LocationUpdatesService
  9. Pietro Granati 2018-05-17

    Hey guys! Any news? I'm struggling to get location updates like on android 7 right know I'm blocked
  10. Joshua Quick 2018-05-17

    The current plan is to add this feature to Titanium 7.3.0, but we don't have an ETA for when that will be released yet. You may want to try the module mentioned by Greg above. I haven't tried it, but I can see in its code that it calls Service.startForeground() which should do the job.
  11. greg 2018-05-17

    As far as I can tell the AppWerft code is not currently working. It's close... but not quite...
  12. Pietro Granati 2018-05-21

    I appreciate your answer Joshua, but for me background process is the key for some of my apps, new clients with android 8 can't use it. SInce 31 people start watching this, I think it's time to set priority high.
  13. Abir Mukherjee 2018-05-23

    Bumping priority. Moving to 7.4.0 to be worked on as first priority.
  14. Abir Mukherjee 2018-05-23

    Thanks [~jquick], we'll move it back to 7.3.0.
  15. Joshua Quick 2018-05-26

    PR (master): https://github.com/appcelerator/titanium_mobile/pull/10076
  16. Joshua Quick 2018-05-26

    *Note to everyone:* If you start a "Service", but never stop it upon app exit, then the service will keep the prior JavaScript runtime alive as per the design choice made by [TIMOB-9831]. The negative consequence of this is that when you attempt to restart the app (tap on the app in the app list), then you'll be stuck on the splash screen and the "app.js" won't get loaded until the service has been stopped. Only one JavaScript runtime can exist at a time and we can't spawn a new one until the service has released the last one. (I don't necessarily agree with this design, but changing it now would be a "breaking-change" for those who depend on it.) That said, there are 2 solutions to address this concern. *Solution 1:* Stop the service upon app exit. You can do this by listening for the "close" event from the root window as follows.
        var intent = Ti.Android.createServiceIntent({
        	url: "Service.js",
        });
        Ti.Android.startService(intent);
        
        var window = Ti.UI.createWindow();
        window.addEventListener("close", function(e) {
        	Ti.Android.stopService(intent);
        });
        window.open();
        
    *Solution 2:* Prevent the root window from being closed by listening for its "androidback" event and use an intent to go to the home-screen (simulates a Home button press). This effectively makes your Android app work more like iOS.
        var window = Ti.UI.createWindow();
        var label = Ti.UI.createLabel({ text: "1" });
        window.add(label);
        window.addEventListener("androidback", function(e) {
        	// If the back key was press, cancel it and go to the home-screen instead.
        	var intent = Ti.Android.createIntent({
        		action: Ti.Android.ACTION_MAIN,
        	});
        	intent.addCategory(Ti.Android.CATEGORY_HOME);
        	intent.setFlags(Ti.Android.FLAG_ACTIVITY_NEW_TASK);
        	Ti.Android.currentActivity.startActivity(intent);
        });
        window.open();
        
        setInterval(function() {
        	label.text = (Number(label.text) + 1).toString();
        }, 1000);
        
  17. Hans Knöchel 2018-05-27

    I have cherry-picked the changes from [#10076](https://github.com/appcelerator/titanium_mobile/pull/10076) and [#9945](https://github.com/appcelerator/titanium_mobile/pull/9945) into 7_1_X, so developers can use it with their stable GA until 7.3.0 gets released. Important notes before downloading the custom SDK: - It is "7.1.2", a 7.1.1.GA + the changes of this ticket - The changes have not passed the QE-validation so far - The custom SDK is no official build, so please use with caution! Download: https://www.dropbox.com/s/k9n73ow64hq9f51/mobilesdk-7.1.2-osx.zip?dl=1
  18. Pietro Granati 2018-05-28

    Hans you made my day!
  19. pako.artal@htg-express.com 2018-05-28

    Hi, Thanks a lot for the fix and for the cherry pick! I tested the cherry picked sdk with 4 phones : * Samsung Galaxy J5 / Android 7.1.1 * BQ Aquaris M5 / android 7. * Google pixel 2 / Android 8 * Sony Xperia F8331 / Android 8.0.0 With the same code It's working for many hours in the the Samsung, BQ and Pixel, but for the Sony I got this error:
        [ERROR] :  KrollProxy: (main) [2370,36837] Error creating proxy
        [ERROR] :  KrollProxy: java.lang.IllegalStateException: Creation arguments must contain intent
        [ERROR] :  KrollProxy: 	at ti.modules.titanium.android.PendingIntentProxy.handleCreationArgs(PendingIntentProxy.java:58)
        [ERROR] :  KrollProxy: 	at org.appcelerator.kroll.KrollProxy.setupProxy(KrollProxy.java:129)
        [ERROR] :  KrollProxy: 	at org.appcelerator.kroll.KrollProxy.createProxy(KrollProxy.java:138)
        [ERROR] :  KrollProxy: 	at org.appcelerator.kroll.runtime.v8.V8Runtime.nativeRunModule(Native Method)
        [ERROR] :  KrollProxy: 	at org.appcelerator.kroll.runtime.v8.V8Runtime.doRunModule(V8Runtime.java:187)
        [ERROR] :  KrollProxy: 	at org.appcelerator.kroll.KrollRuntime.runModule(KrollRuntime.java:247)
        [ERROR] :  KrollProxy: 	at ti.modules.titanium.android.TiJSService.executeServiceCode(TiJSService.java:74)
        [ERROR] :  KrollProxy: 	at ti.modules.titanium.android.TiJSService.start(TiJSService.java:96)
        [ERROR] :  KrollProxy: 	at ti.modules.titanium.android.TiJSService.onStartCommand(TiJSService.java:47)
        [ERROR] :  KrollProxy: 	at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:3544)
        [ERROR] :  KrollProxy: 	at android.app.ActivityThread.-wrap20(Unknown Source:0)
        [ERROR] :  KrollProxy: 	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1703)
        [ERROR] :  KrollProxy: 	at android.os.Handler.dispatchMessage(Handler.java:105)
        [ERROR] :  KrollProxy: 	at android.os.Looper.loop(Looper.java:251)
        [ERROR] :  KrollProxy: 	at android.app.ActivityThread.main(ActivityThread.java:6572)
        [ERROR] :  KrollProxy: 	at java.lang.reflect.Method.invoke(Native Method)
        [ERROR] :  KrollProxy: 	at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
        [ERROR] :  KrollProxy: 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
        [ERROR] :  JavaObject: !!! OH NO! We tried to grab a Java Object back out of the reference table, but it must have been GC'd, because it's null! Key: 131
        [WARN] :   TiNotification: (main) [5,36842] Falling back to default notification channel.
        [WARN] :   TiNotification: It is highly advised to create your own notification channel using Ti.Android.NotificationManager.createNotificationChannel()
        [ERROR] :  KrollProxy: (main) [0,36842] Error creating proxy
        [ERROR] :  KrollProxy: java.lang.NullPointerException: Attempt to invoke virtual method 'android.app.PendingIntent ti.modules.titanium.android.PendingIntentProxy.getPendingIntent()' on a null object reference
        [ERROR] :  KrollProxy: 	at ti.modules.titanium.android.notificationmanager.NotificationProxy.setContentIntent(NotificationProxy.java:304)
        [ERROR] :  KrollProxy: 	at ti.modules.titanium.android.notificationmanager.NotificationProxy.handleCreationDict(NotificationProxy.java:96)
        [ERROR] :  KrollProxy: 	at org.appcelerator.kroll.KrollProxy.handleCreationArgs(KrollProxy.java:197)
        [ERROR] :  KrollProxy: 	at org.appcelerator.kroll.KrollProxy.setupProxy(KrollProxy.java:129)
        [ERROR] :  KrollProxy: 	at org.appcelerator.kroll.KrollProxy.createProxy(KrollProxy.java:138)
        [ERROR] :  KrollProxy: 	at org.appcelerator.kroll.runtime.v8.V8Runtime.nativeRunModule(Native Method)
        [ERROR] :  KrollProxy: 	at org.appcelerator.kroll.runtime.v8.V8Runtime.doRunModule(V8Runtime.java:187)
        [ERROR] :  KrollProxy: 	at org.appcelerator.kroll.KrollRuntime.runModule(KrollRuntime.java:247)
        [ERROR] :  KrollProxy: 	at ti.modules.titanium.android.TiJSService.executeServiceCode(TiJSService.java:74)
        [ERROR] :  KrollProxy: 	at ti.modules.titanium.android.TiJSService.start(TiJSService.java:96)
        [ERROR] :  KrollProxy: 	at ti.modules.titanium.android.TiJSService.onStartCommand(TiJSService.java:47)
        [ERROR] :  KrollProxy: 	at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:3544)
        [ERROR] :  KrollProxy: 	at android.app.ActivityThread.-wrap20(Unknown Source:0)
        [ERROR] :  KrollProxy: 	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1703)
        [ERROR] :  KrollProxy: 	at android.os.Handler.dispatchMessage(Handler.java:105)
        [ERROR] :  KrollProxy: 	at android.os.Looper.loop(Looper.java:251)
        [ERROR] :  KrollProxy: 	at android.app.ActivityThread.main(ActivityThread.java:6572)
        [ERROR] :  KrollProxy: 	at java.lang.reflect.Method.invoke(Native Method)
        [ERROR] :  KrollProxy: 	at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
        [ERROR] :  KrollProxy: 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
        [ERROR] :  JavaObject: !!! OH NO! We tried to grab a Java Object back out of the reference table, but it must have been GC'd, because it's null! Key: 132
        [WARN] :   W/System.err: java.lang.RuntimeException: Notification object argument cannot be null.
        [WARN] :   W/System.err: 	at org.appcelerator.titanium.proxy.ServiceProxy.foregroundNotify(ServiceProxy.java:136)
        [WARN] :   W/System.err: 	at org.appcelerator.kroll.runtime.v8.V8Runtime.nativeRunModule(Native Method)
        [WARN] :   W/System.err: 	at org.appcelerator.kroll.runtime.v8.V8Runtime.doRunModule(V8Runtime.java:187)
        [WARN] :   W/System.err: 	at org.appcelerator.kroll.KrollRuntime.runModule(KrollRuntime.java:247)
        [WARN] :   W/System.err: 	at ti.modules.titanium.android.TiJSService.executeServiceCode(TiJSService.java:74)
        [WARN] :   W/System.err: 	at ti.modules.titanium.android.TiJSService.start(TiJSService.java:96)
        [WARN] :   W/System.err: 	at ti.modules.titanium.android.TiJSService.onStartCommand(TiJSService.java:47)
        [WARN] :   W/System.err: 	at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:3544)
        [WARN] :   W/System.err: 	at android.app.ActivityThread.-wrap20(Unknown Source:0)
        [WARN] :   W/System.err: 	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1703)
        [WARN] :   W/System.err: 	at android.os.Handler.dispatchMessage(Handler.java:105)
        [WARN] :   W/System.err: 	at android.os.Looper.loop(Looper.java:251)
        [WARN] :   W/System.err: 	at android.app.ActivityThread.main(ActivityThread.java:6572)
        [WARN] :   W/System.err: 	at java.lang.reflect.Method.invoke(Native Method)
        [WARN] :   W/System.err: 	at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
        [WARN] :   W/System.err: 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
        [ERROR] :  TiExceptionHandler: (main) [2,36844] ----- Titanium Javascript Runtime Error -----
        [ERROR] :  TiExceptionHandler: (main) [0,36844] - In /bGeo/Ti.Geo.Timer.js:13,27
        [ERROR] :  TiExceptionHandler: (main) [0,36844] - Message: Uncaught Notification object argument cannot be null.
        [ERROR] :  TiExceptionHandler: (main) [0,36844] - Source: Ti.Android.currentService.foregroundNotify(notificationId,Ti.Android.createNotification({
        [ERROR] :  V8Exception: Exception occurred at /bGeo/Ti.Geo.Timer.js:13: Uncaught Notification object argument cannot be null.
        [ERROR] :  V8Exception: Notification object argument cannot be null.
        
    My code:
        var notificationId = 123;
        Ti.Android.currentService.foregroundNotify(notificationId, Ti.Android.createNotification({
        	contentTitle : "XXXXX",
        	contentText : "Localizando ...",
        	contentIntent : Ti.Android.createPendingIntent({
        		// Using the launch intent will resume the app when tapped.
        		intent : Ti.App.Android.launchIntent,
        	}),
        }));
        
    In my tiapp.xml I have.
        <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="26"/>
        <services>
            <service url="bGeo/Ti.Geo.Timer.js"/>
        </services>
        
    Should I have created a Notification Channel ?
  20. Joshua Quick 2018-05-29

    [~pako.artal@htg-express.com], it sounds like the launchIntent property is returning null. We'll look into it.
  21. Joshua Quick 2018-05-29

    And ideally, yes, you should create your own notification channel on Android 8.0 and above. That way you can control the name and importance level given to it, as seen by the end-user. But this is not the cause of the error that you are seeing.
        var channel = null;
        if (Ti.Platform.Android.API_LEVEL >= 26) {
        	channel = Ti.Android.NotificationManager.createNotificationChannel({
        		// Unique string ID you assign to the channel.
        		id: "my_channel_id",
        		// Channel name seen by the end-user.
        		name: "My Channel Name",
        		importance: Ti.Android.IMPORTANCE_DEFAULT,
        	});
        }
        Ti.Android.currentService.foregroundNotify(123, Ti.Android.createNotification({
        	contentTitle: "Foreground Service",
        	contentText: "Content Text",
        	channelId: channel ? channel.id : null,
        	contentIntent: Ti.Android.createPendingIntent({
        		// Using the launch intent will resume the app when tapped.
        		intent: Ti.App.Android.launchIntent,
        	}),
        }));
        
  22. Joshua Quick 2018-05-29

    [~pako.artal@htg-express.com], After some investigation, it turns out the native Java [Android.getActivity()](https://developer.android.com/reference/android/app/Activity#getIntent()) method can in fact return null in rare cases. Looks like you're running into one of them. This is the method that Titanium's Ti.App.Android.launchIntent and Ti.Android.Activity.intent properties acquire the launch intent from. https://stackoverflow.com/questions/37856407/can-activity-getintent-ever-return-null So, a simple solution to this problem is to set up the notification to not do anything when tapped on. You can do this by setting up the pending-intent with an empty intent like the below. Note that the foreground notification won't disappear when tapped on, making this a viable solution.
        contentIntent: Ti.Android.createPendingIntent({
        	intent: Ti.Android.createIntent({}),
        }),
        
    Or you can do something like the below. Check if launchIntent is null, and if it is, use an empty intent instead.
        function getAppResumeIntent() {
        	var intent = Ti.App.Android.launchIntent;
        	if (!intent) {
        		intent = Ti.Android.createIntent({});
        	}
        	return intent;
        }
        Ti.Android.currentService.foregroundNotify(123, Ti.Android.createNotification({
        	contentTitle: "Foreground Service",
        	contentText: "Content Text",
        	contentIntent: Ti.Android.createPendingIntent({
        		intent: getAppResumeIntent(),
        	}),
        }));
        
  23. engwei 2018-06-06

    Thanks for your work on this! Is it possible to update the notification text after it has been set? I was trying to change the text every time a location is updated with something like 'Last location update 15:12' but it doesn't seem to do it consistently.
  24. Joshua Quick 2018-06-06

    {quote}Is it possible to update the notification text after it has been set?{quote} [~engwei], yes, you can update the notification's text by calling the foregroundNotify() method again with the same notification ID you used before. This means that you cannot simply just update the notification object's text. The updated notification object has to be re-posted to the status bar via a call to foregroundNotify(). This is how it works at the native level. Also note that the same is true for notifications posted via NotificationManager.notify(). If you want to update a notification posted via NotificationManager, then you must call its notify() method again with the same notification ID you used before. We made sure to make the behavior consistent for foreground services. I hope this helps!
  25. engwei 2018-06-06

    Thanks Joshua that works perfectly. I was trying to use setLatestEventInfo() and that seemed to only work periodically. Your method works perfectly. Thanks
  26. engwei 2018-06-08

    I received this crash today which I think looks like it's related to foreground service: java.lang.RuntimeException: Unable to resume activity {com.ingelby.grititranger/org.appcelerator.titanium.TiActivity}: java.lang.NumberFormatException: s == null at android.app.ActivityThread.performResumeActivity(ActivityThread.java:3788) at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3828) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1746) at android.os.Handler.dispatchMessage(Handler.java:105) at android.os.Looper.loop(Looper.java:164) at android.app.ActivityThread.main(ActivityThread.java:6938) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374) Caused by: java.lang.NumberFormatException: s == null at java.lang.Integer.parseInt(Integer.java:570) at java.lang.Integer.parseInt(Integer.java:643) at com.appcelerator.aps.APSAnalyticsModel.addEvent(APSAnalyticsModel.java:78) at com.appcelerator.aps.APSAnalyticsHelper.sendAnalyticsEvent(APSAnalyticsHelper.java:518) at com.appcelerator.aps.APSAnalyticsHelper.postAnalyticsEvent(APSAnalyticsHelper.java:506) at com.appcelerator.aps.APSAnalytics.sendAppForegroundEvent(APSAnalytics.java:133) at org.appcelerator.titanium.TiBaseActivity.onResume(TiBaseActivity.java:1323) at org.appcelerator.titanium.TiActivity.onResume(TiActivity.java:65) at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1361) at android.app.Activity.performResume(Activity.java:7344) at android.app.ActivityThread.performResumeActivity(ActivityThread.java:3763)
  27. Joshua Quick 2018-06-08

  28. Lokesh Choudhary 2018-06-19

    Verified the fix in SDK 7.3.0.v20180618182516. Closing.

JSON Source