Skip to Content
Technical Articles
Author's profile photo Daniel Van Leeuwen

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.

  1. 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.
  2. 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.
  3. Next, we need to create the service. To do so, create a new file called OfflineODataForegroundService.java in the main package.
  4. 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
                }
            }
        }
    }
    
  5. 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;
    
  6. 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;
        }
    };
  7. 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();
    }
    
  8. Add code to call doBindService(); in MainActivity‘s onCreate method before the call to onRegister(null).
  9. The OfflineODataForegroundService class wraps the offline store’s open, upload and download operations, so next, replace the calls to these methods in MainActivity.java with the wrapped versions, like so:
    • Replace myOfflineDataProvider.open(...); with boundService.openStore(myOfflineDataProvider, ...);
    • Replace myOfflineDataProvider.upload(...); with boundService.uploadStore(myOfflineDataProvider, ...);
    • Replace myOfflineDataProvider.download(...); with boundService.downloadStore(myOfflineDataProvider, ...);

    Where the … in each case represents the success and failure callbacks that you pass to the operations.

  10. 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>

  11. 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)

Assigned Tags

      Be the first to leave a comment
      You must be Logged on to comment or reply to a post.