Using Android Background Services in NativeScript


Important: This article will not teach you what background services in Android are, or how to use them. If you’d like to acquaint yourself with services, head to the Android API Guides.

Hey, since it’s my first blog post, a proper introduction is in order. My name is Peter, I work as a software developer, and am part of the core Android Runtime team in NativeScript. Today I’ll show you how Android Background Services behave in a NativeScript application, and provide guidelines to writing your own implementations.

Services in Android

When your application needs to perform heavy, non-UI work, you must take care not to block the UI thread and disrupt the otherwise smooth user experience. On Android, using background services is the right technique for the job.

Common examples of heavy non-UI work are

  • fetching data from a remote backend (mail, chat messages)*
  • writing data into a local DB**
  • downloading/uploading an image*
  • firing a scheduled notification
  • getting current GPS location
  • communicating with other applications on the device

* - the core modules package already comes with ready-to-use solutions for Async Http Client, Async handling of images, and more

** - the NativeScript open source community creates plugins that enable developers to work with SQLite and other NOSQL databases

Services in NativeScript

Almost everything that you can do when developing with Android - you can do with NativeScript too. Due to the nature of the NativeScript Android Runtime - where the JavaScript code runs on the main UI thread, there are certain known limitations when using Android services.

  • IntentService’s onHandleIntent will execute on the main UI thread (versus on a dedicated worker Thread when implemented in pure Android)
  • IntentService’s implementation needs to use a constructor that takes no arguments, but that currently is not possible through Java.
  • The base Service class and all its descendants execute on the main UI thread by default in Android. Implementing a service that should run on a background thread must be managed in Java by the developer (see Notes).
  • BroadcastReceivers should not be registered with the android: process =":remote" property in the AndroidManifest as the receiver will be unable to execute JavaScript in an independent process

Below is an example of how we can use the Android Alarm Manager to schedule calling an IntentService which will create Notifications even when the Application’s Activities have been destroyed (the application has been closed). We will leverage one of NativeScript’s perks - directly access native (Java) API with JavaScript in order to write code that is as close as possible to that which you will find on the internet when looking up Android Guides. Original code SO

  1. Create a default NativeScript application
    tns create my-app
    cd my-app
     
  2. Extend the android.app.IntentService class and describe the work that needs to be done in the onHandleIntent method callback. In this case we want the class to create a notification object using the Notification Builder, and send it to the Notifications Service. Create a new file called NotificationIntentService.js and write the following:
    android.app.IntentService.extend("com.tns.notifications.NotificationIntentService" /* give your class a valid name as it will need to be declared in the AndroidManifest later */, {
       onHandleIntent: function (intent) {
           var action = intent.getAction();
           if ("ACTION_START" == action) {
               postNotification();
           } else if (“ACTION_STOP” == action) {
        /* get the system alarm manager and cancel all pending alarms, which will stop the service from executing periodically  */
           }
        
           android.support.v4.content.WakefulBroadcastReceiver.completeWakefulIntent(intent);

     

       }
    });
    function postNotification() {
       // Do something. For example, fetch fresh data from backend to create a rich notification?
       var utils = require("utils/utils");
       var context = utils.ad.getApplicationContext(); // get a reference to the application context in Android
       var builder = new android.app.Notification.Builder(context);
       builder.setContentTitle("Scheduled Notification")
           .setAutoCancel(true)
           .setColor(android.R.color.holo_purple) // optional
           .setContentText("This notification has been triggered by Notification Service")
           .setVibrate([100, 200, 100]) // optional
           .setSmallIcon(android.R.drawable.btn_star_big_on);
           // will open main NativeScript activity when the notification is pressed
       var mainIntent = new android.content.Intent(context, com.tns.NativeScriptActivity.class);
       var pendingIntent = android.app.PendingIntent.getActivity(context,
           1,
           mainIntent,
           android.app.PendingIntent.FLAG_UPDATE_CURRENT);
       builder.setContentIntent(pendingIntent);
       builder.setDeleteIntent(getDeleteIntent(context));
       var manager = context.getSystemService(android.content.Context.NOTIFICATION_SERVICE);
       manager.notify(1, builder.build());
    }
    /* only necessary for dismissing the notification from the Notifications Screen */
    function getDeleteIntent(context) {
           var intent = new android.content.Intent(context, com.tns.broadcastreceivers.NotificationEventReceiver.class);
           intent.setAction("ACTION_DELETE_NOTIFICATION");
           return android.app.PendingIntent.getBroadcast(context, 0, intent, android.app.PendingIntent.FLAG_UPDATE_CURRENT);
    }
     
  3. Create a BroadcastReceiver that listens for events fired from the system or the Alarm Manager and starts the Service. We will only implement the intent with action (ACTION_START) for posting notifications.The same logic is applicable if you want to stop the service. Using WakefulBroadcastReceiver instead of a BroadcastReceiver guarantees the CPU will not sleep until completion of request processing in the Service(until the IntentService calls completeWakefulIntent)
    android.support.v4.content.WakefulBroadcastReceiver.extend("com.tns.broadcastreceivers.NotificationEventReceiver", {
       onReceive: function (context, intent) {
           var action = intent.getAction();
           var serviceIntent;
           if ("ACTION_START_NOTIFICATION_SERVICE" == action) {
               serviceIntent = createIntentStartNotificationService(context);
           } else if ("ACTION_DELETE_NOTIFICATION" == action) {
               serviceIntent = createIntentDeleteNotification(context);
           }
           if (serviceIntent) {
               android.support.v4.content.WakefulBroadcastReceiver.startWakefulService(context, serviceIntent);
           }
       }
    })
    var Intent = android.content.Intent;
    function createIntentStartNotificationService(context) {
       var intent = new Intent(context, com.tns.notifications.NotificationIntentService.class);
       intent.setAction("ACTION_START");
       return intent;
    }
    function createIntentDeleteNotification(context) {
       /* Similar as above, just with a different action */
    }
     
  4. Now we need to create the actual alarm which will periodically send intents to our WakefulBroadcastReceiver. Create a new file called service-helper.js. Create a setupAlarm function and export it, so that it can be called from anywhere in our code.
    function getStartPendingIntent(context) {
       var alarmIntent = new android.content.Intent(context, com.tns.broadcastreceivers.NotificationEventReceiver.class);
       intent.setAction("ACTION_START_NOTIFICATION_SERVICE");
       return android.app.PendingIntent.getBroadcast(context, 0, intent, android.app.PendingIntent.FLAG_UPDATE_CURRENT);
    }
    function setupAlarm(context) {
       var alarmManager = context.getSystemService(android.content.Context.ALARM_SERVICE);
       var alarmIntent = getStartPendingIntent(context);
       alarmManager.setRepeating(android.app.AlarmManager.RTC_WAKEUP,
           java.lang.System.currentTimeMillis(),
           1000 * 60 * 60 * 24, /* alarm will send the `alarmIntent` object every 24h */
           alarmIntent);
    }
    Function cancelAlarm(context) { … }
    module.exports.setupAlarm = setupAlarm;
     
  5. Next - let’s call setupAlarm on button click. In the main-view-model.js script of your application, change the onTap event to the following:
    function createViewModel() {
       var utils = require("utils/utils");
       var services = require("./service-helper");
       /* … */
       viewModel.onTap = function () {
           services.setupAlarm(utils.ad.getApplicationContext());
       }
       /* insert your code for cancelling the alarm manager on user interaction */
       return viewModel;
    }
     
  6. And we are almost set! Finally, we need to register our service, and BroadcastReceivers in the AndroidManifest.xml located in app/App_Resources/Android
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="__PACKAGE__"
        android:versionCode="1"
        android:versionName="1.0">
        <supports-screens
            android:smallScreens="true"
            android:normalScreens="true"
            android:largeScreens="true"
            android:xlargeScreens="true"/>
        <uses-sdk
            android:minSdkVersion="17"
            android:targetSdkVersion="__APILEVEL__"/>
        <!-- all user-permissions are declared above →
        <!-- using the WakefulBroadcastReceiver requires that a WAKE_LOCK permission is granted -->
               <uses-permission android:name="android.permission.WAKE_LOCK" />
        <application ... >
            <activity
                ...
    </intent-filter>
            </activity>
            <activity android:name="com.tns.ErrorReportActivity"/>
            
    <service
               android:name="com.tns.notifications.NotificationIntentService"
               android:enabled="true"
               android:exported="false" />
            <receiver android:name="com.tns.broadcastreceivers.NotificationEventReceiver" />
        </application>
    </manifest>

Now you are ready to test away your notifications scheduler!

Conclusion

We have used IntentService to perform operations without a foreground activity. Keep in mind, the code executes on the main thread of a NativeScript Android app. A more intensive operation, like downloading an image, could possibly  block the foreground activity- thus freezing the user interface, or throw an exception for violating the strict thread policy in Android. Consider using background services for almost immediate-result operations.

Note: Be very mindful when developing applications which send out notifications. Some users may find them annoying, if they get them too often, and as a result - delete your application.

Note: The suggested approach when you want to offload a long-running job on a different Thread is to write the implementation in Java classes and invoke the job in NativeScript.

What’s next

In the upcoming releases we will be rolling out support for the Web Workers feature in NativeScript, allowing you to execute code in a background thread, all through Angular/JavaScript.

 

Useful resources:

Author

Peter Kanev

Comments


Comments are disabled in preview mode.
NativeScript is licensed under the Apache 2.0 license
© 2020 All Rights Reserved.