Technical Articles
Step by Step with the SAP Cloud Platform SDK for Android — Part 6b — Using a Foreground Service when Syncing
Previous (Syncing an Offline Store) Home Next (Using ILOData)
An offline application may take some time to upload and download changes when performing a sync. If the user navigates away from the app while a sync is occurring, there is a chance that the process will be terminated when low on memory. Attaching the sync operation to a Foreground Service addresses this problem because foreground services can continue to execute their tasks after the app is minimized. Foreground services also show the user an informative notification.
Here is the end result of the foreground service that this section will add to the app.
The following steps demonstrate how to add the foreground service, and then attach the sync operation.
- The first step is adding an image to the project to represent the sync action. To do so, right click on the res folder, and choose New > Image Asset.
- Next, in the resulting Asset Studio window, fill in the properties as shown below, search for download, and click “Next”, and then click “Finish”. The image will be added to your project.
- Next, we need to create the service. To do so, create a new file called
OfflineODataForegroundService.java
in the main package. - Replace the contents of
OfflineODataForegroundService.java
with the below code:package com.sap.stepbystep; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Intent; import android.os.Build; import android.os.IBinder; import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; import com.sap.cloud.mobile.odata.core.Action0; import com.sap.cloud.mobile.odata.core.Action1; import com.sap.cloud.mobile.odata.offline.OfflineODataException; import com.sap.cloud.mobile.odata.offline.OfflineODataProvider; import java.util.ArrayDeque; import java.util.Queue; public class OfflineODataForegroundService extends Service { private static final int NOTIFICATION_ID = 1; private static final String CHANNEL_ID = "Offline OData Channel"; private static final String CANCEL_ACTION = "offline.odata.action.cancel"; final Queue<Task> tasks = new ArrayDeque<>(); private Action1<Task> taskCompleteHandler = new Action1<Task>() { @Override public void call(Task task) { synchronized (tasks) { tasks.remove(task); if (tasks.poll() == null) { stopForeground(true); } } } }; // This is the object that receives interactions from clients. private final IBinder binder = new LocalBinder(); /** * Class for clients to access. Because we know this service always * runs in the same process as its clients, we don't need to deal with * IPC. */ public class LocalBinder extends android.os.Binder { public OfflineODataForegroundService getService() { return OfflineODataForegroundService.this; } } @Override public void onCreate() { super.onCreate(); createNotificationChannel(); } @Override public void onDestroy() { super.onDestroy(); } @Override public IBinder onBind(Intent intent) { return binder; } @Override public int onStartCommand(Intent intent, int flags, int startId) { String action = intent.getAction(); if (action.equals(CANCEL_ACTION)) { cancelTasks(); } return START_NOT_STICKY; } private void startTask(Task task) { synchronized (tasks) { if (tasks.poll() == null) { startForeground(NOTIFICATION_ID, createNotification()); } tasks.add(task); } task.run(); } private void cancelTasks() { synchronized (tasks) { for (Task task : tasks) { task.cancel(); } } } public void openStore(OfflineODataProvider offlineODataProvider, @Nullable final Action0 successHandler, @Nullable final Action1<OfflineODataException> failureHandler) { Task task = new Task(Operation.OPEN, offlineODataProvider, successHandler, failureHandler); startTask(task); } public void downloadStore(OfflineODataProvider offlineODataProvider, @Nullable final Action0 successHandler, @Nullable final Action1<OfflineODataException> failureHandler) { Task task = new Task(Operation.DOWNLOAD, offlineODataProvider, successHandler, failureHandler); startTask(task); } public void uploadStore(OfflineODataProvider offlineODataProvider, @Nullable final Action0 successHandler, @Nullable final Action1<OfflineODataException> failureHandler) { Task task = new Task(Operation.UPLOAD, offlineODataProvider, successHandler, failureHandler); startTask(task); } private Notification createNotification() { NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID); NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle(); bigTextStyle.setBigContentTitle("Syncing Data."); builder.setStyle(bigTextStyle); builder.setWhen(System.currentTimeMillis()); builder.setSmallIcon(R.drawable.ic_downloading); builder.setProgress(100, 0, true); // Clicking the notification will return to the app Intent intent = new Intent(this, MainActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0); builder.setFullScreenIntent(pendingIntent, false); // // Add cancel action // Intent cancelIntent = new Intent(this, OfflineODataForegroundService.class); // cancelIntent.setAction(CANCEL_ACTION); // PendingIntent pendingCancelIntent = PendingIntent.getService(this, 0, cancelIntent, PendingIntent.FLAG_UPDATE_CURRENT); // builder.addAction(android.R.drawable.ic_menu_delete, "Cancel", pendingCancelIntent); return builder.build(); } private void createNotificationChannel() { // Create the NotificationChannel, but only on API 26+ because // the NotificationChannel class is new and not in the support library if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { CharSequence name = "Offline Sync"; int importance = NotificationManager.IMPORTANCE_DEFAULT; NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance); NotificationManager notificationManager = getSystemService(NotificationManager.class); channel.setSound(null, null); notificationManager.createNotificationChannel(channel); } } private enum Operation { OPEN, DOWNLOAD, UPLOAD } private class Task implements Runnable { private OfflineODataProvider provider; private Action0 successHandler; private Action1<OfflineODataException> failureHandler; private Operation operation; private Task(Operation operation, OfflineODataProvider provider, Action0 successHandler, Action1<OfflineODataException> failureHandler) { this.operation = operation; this.provider = provider; this.successHandler = successHandler; this.failureHandler = failureHandler; } public void run() { Action0 success = new Action0() { @Override public void call() { taskCompleteHandler.call(Task.this); if (successHandler != null) { successHandler.call(); } } }; Action1<OfflineODataException> failure = new Action1<OfflineODataException>() { @Override public void call(OfflineODataException e) { taskCompleteHandler.call(Task.this); if (failureHandler != null) { failureHandler.call(e); } } }; switch(this.operation) { case OPEN: this.provider.open(success, failure); break; case UPLOAD: this.provider.upload(success, failure); break; case DOWNLOAD: this.provider.download(success, failure); break; } } public void cancel() { try { if (this.operation == Operation.OPEN) { this.provider.cancelDownload(); } else { this.provider.cancelUpload(); } } catch (OfflineODataException ex) { // Failed to cancel } } } }
- Now that the service has been added to the project, you need to make some changes to the
MainActivity
to use it. First, add these two global variables.// Don't attempt to unbind from the service unless the client has received some // information about the service's state. private boolean shouldUnbind; // To invoke the bound service, first make sure that this value is not null. private OfflineODataForegroundService boundService;
- Next, add an implementation of the
ServiceConnection
interface, by pasting the below code in the list of global variables.private ServiceConnection connection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { // This is called when the connection with the service has been // established, giving us the service object we can use to // interact with the service. Because we have bound to a explicit // service that we know is running in our own process, we can // cast its IBinder to a concrete class and directly access it. boundService = ((OfflineODataForegroundService.LocalBinder)service).getService(); } public void onServiceDisconnected(ComponentName className) { // This is called when the connection with the service has been // unexpectedly disconnected -- that is, its process crashed. // Because it is running in our same process, we should never // see this happen. boundService = null; } };
- Add the following two methods to
MainActivity.java
. These methods attach the service to the activity.void doBindService() { // Attempts to establish a connection with the service. if (bindService(new Intent(MainActivity.this, OfflineODataForegroundService.class), connection, Context.BIND_AUTO_CREATE)) { shouldUnbind = true; } else { Log.e(myTag, "Error: The requested service doesn't " + "exist, or this client isn't allowed access to it."); } } void doUnbindService() { if (shouldUnbind) { // Release information about the service's state. unbindService(connection); shouldUnbind = false; } } @Override protected void onDestroy() { super.onDestroy(); doUnbindService(); }
- Add code to call
doBindService();
inMainActivity
‘sonCreate
method before the call to onRegister(null). - The
OfflineODataForegroundService
class wraps the offline store’s open, upload and download operations, so next, replace the calls to these methods inMainActivity.java
with the wrapped versions, like so:- Replace
myOfflineDataProvider.open(...);
withboundService.openStore(myOfflineDataProvider, ...);
- Replace
myOfflineDataProvider.upload(...);
withboundService.uploadStore(myOfflineDataProvider, ...);
- Replace
myOfflineDataProvider.download(...);
withboundService.downloadStore(myOfflineDataProvider, ...);
Where the … in each case represents the success and failure callbacks that you pass to the operations.
- Replace
- Finally, add the required permission to use foreground services, and the actual service definition to
AndroidManifest.xml
The required permission is:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
And the required service definition is:<service android:name="com.sap.stepbystep.OfflineODataForegroundService"> </service>
- Run the app and click the “Perform Sync” button. Notice now that a notification appears while the sync is taking place, and the app can be minimized while syncing.
Previous (Syncing an Offline Store) Home Next (Using ILOData)