Create a sync extension for IoTool using HTTP

This document will talk about developing sync extension using HTTP connection. The example provided in the guide was used to develop InitialState cloud extension to send data to InitialState Cloud.

General things

Before we start developing extensions we need to understand, that there is a possibility that some of the developed extension could be used worldwide. Therefore me need to ensure that the extension do not interfere with each other. Because of that we need make sure that the packages, classes etc. A general idea was described in this document, however this document is used for sensor extensions. Because of that we need to address differences between then. First of there is no service ID to name. Second, package names, class names, etc. should contain name that resembles service ID and should follow the same naming rules. In this example is "InitialStateCloud". Sync extension also has to use the same classes (service, provider and preferences). Both have similar implementation of preferences and provider (Sync extension has less options to set in provider, but usually a lot more preferences to set).

Note: Further instances of your chosen name will be referred as serviceID.

How to proceed

Just make a copy of the demo project and make sure that you change names that contain "InitialStateCloud" to you chosen name.

Note: Any change to the code may be needed only at the sections which start with "//TODO".

Libraries

Demo project uses 3 libraries made by us:

  • IoToolLibrarySync.arr

  • IoToolLibraryBase.arr

  • IoToolLibraryComms.arr

Classes

This source contains additional classes that are used for easier constructing data to send to the cloud.

"IoToolSyncService"+ serviceID +"IoT.java"

Define and implements all necessary setup and processing of data to send. In demo code packages this class already implements all extension functionality such as connecting to a cloud, responding to the application commands etc.

package io.senlab.iotool.extension.initialstatecloud;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.database.sqlite.SQLiteDatabase;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import android.widget.Toast;

import com.google.gson.Gson;

import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.LinkedList;

import javax.net.ssl.HttpsURLConnection;

import io.senlab.iotool.library.base.IoToolConstants;
import io.senlab.iotool.library.sync.SyncConstants;
import io.senlab.iotool.library.sync.SyncFunctions;
import io.senlab.iotool.library.sync.SyncItem;
import io.senlab.iotool.library.sync.SyncList;
import io.senlab.iotool.library.sync.SyncMonitorFunctions;
import io.senlab.iotool.library.sync.SyncProperties;

public class IoToolSyncServiceInitialStateCloud extends Service {

    HttpURLConnection client;
    DataOutputStream stream;
    private final String BASE_DATA_URL = "events";
    private final String METHOD_TYPE = "POST";
    private final String CONTENT_TYPE_KEY = "Content-Type";
    private final String CONTENT_TYPE_VALUE = "application/json";
    private final String X_ACCESS_KEY = "X-IS-AccessKey";
    private final String ACCEPT_VERSION_KEY = "Accept-Version";
    private final String ACCEPT_VERSION_VALUE = "~0";
    private final String BUCKET_KEY = "X-IS-BucketKey";

    boolean isConnected = false;
    String hostName;
    String bucketKey;
    String accessKey;
    Gson gson = new Gson();
    boolean notShown = true;

    //condensed parameter
    boolean sendCondensedData;

    Handler handlerToast = null; //handles Toast Message

    protected static final String serviceName = "IoToolSyncServiceInitialStateCloud";

    private boolean serviceRunning = false;

    private boolean sendingData = false;
    private boolean sendingDataFiles = false;

    private NotificationManager mNM;

    private Notification notification;
    private PendingIntent contentIntent;
    private ConnectServiceCommandReceiver commandReceiver;

    private SyncList syncList;
    private SyncProperties syncProperties;

    private Handler sendHandler = new Handler();
    private SendRunnable sendRunnable;
    private Handler sendFilesHandler = new Handler();
    private SendFilesRunnable sendFilesRunnable;
    private boolean allFilesSent = false;
    private boolean sendAgain = false;

    private boolean sendDataMode = true;

    private boolean receivingData = false;

    private final long downloadInterval = 10000;
    private final long broadcastInterval = 1000;

    private Handler broadcastHandler = new Handler();
    private BroadcastAndReceiveRunnable broadcastAndReceiveRunnable;

    private long delayBroadcast = 0;

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if(intent == null){
            stop(1);
        }

        Log.v("test","TESTNI IZPIS");

        if (!serviceRunning) {
            try {
                serviceRunning = true;
                commandReceiver = new ConnectServiceCommandReceiver(this);
                registerReceiver(commandReceiver, new IntentFilter(IoToolConstants.ServiceCommand));

                mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);

                CharSequence text = getResources().getString(R.string.service_sync_notif_started);

                // Set the icon, scrolling text and timestamp
                NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
                notification = builder.setContentIntent(contentIntent)
                        .setSmallIcon(R.drawable.status_waiting)
                        .setWhen(System.currentTimeMillis())
                        .setAutoCancel(true)
                        .setContentTitle(serviceName)
                        .setContentText(text)
                        .build();

                // The PendingIntent to launch our activity if the user selects this notification
                contentIntent = PendingIntent.getActivity(this, 0, new Intent(), 0);

                // Set the info for the views that show in the notification panel.

                startForeground(R.string.foreground_service_id, notification);

                sendRunnable = new SendRunnable(this);
                sendFilesRunnable = new SendFilesRunnable(this);

                SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());

                sp.edit().putBoolean(getString(getResources().getIdentifier("preference_key_allowtracking", "string", getPackageName())),intent.getBooleanExtra(IoToolConstants.AllowAnalytics, true)).commit();

                int sendInterval;
                String interval = sp.getString(getString(R.string.preference_key_sendinterval), getString(R.string.preference_default_sendinterval));
                if (interval.trim().endsWith("ms")){
                    sendInterval = Integer.valueOf(interval.substring(0,interval.length()-2));
                }else{
                    sendInterval= getSendIntervalInteger(parseSendInterval(sp.getString(getString(R.string.preference_key_sendinterval), getString(R.string.preference_default_sendinterval)))) * 1000;
                }

                handlerToast = new Handler() {
                    public void handleMessage(Message msg) {
                        if(msg.arg1 == 1)
                            Toast.makeText(getApplicationContext(),getResources().getString(R.string.pref_error_enter_params), Toast.LENGTH_LONG).show();
                    }
                };

                boolean sendFiles = sp.getBoolean(getString(R.string.preference_key_send_files), getResources().getBoolean(R.bool.preference_default_send_files));

                sendDataMode = intent.getBooleanExtra(SyncConstants.SendDataExtra, true);

                boolean liveSession = intent.getBooleanExtra(SyncConstants.LiveSessionExtra, true);
                int session = intent.getIntExtra(SyncConstants.SessionIDExtra, -1);
                long sessionTimeFrom;
                long sessionTimeTo;
                if (session<0) sessionTimeFrom = 0;
                else sessionTimeFrom = intent.getLongExtra(SyncConstants.SessionTimeFromExtra, 0);
                if (liveSession) {
                    Calendar cal = new GregorianCalendar();
                    cal.setTimeInMillis(System.currentTimeMillis());
                    cal.add(Calendar.YEAR, 10);
                    sessionTimeTo = cal.getTimeInMillis();
                } else {
                    sessionTimeTo = intent.getLongExtra(SyncConstants.SessionTimeToExtra, 0);
                    sendInterval = 500;
                }

                File appDir = new File(Environment.getExternalStorageDirectory().toString()+"/"+intent.getStringExtra(IoToolConstants.FileDirExtra));
                if (! appDir.exists()) appDir.mkdir();

                long currentTime = System.currentTimeMillis() - downloadInterval;
                if (intent.hasExtra(IoToolConstants.TimeStampExtra)) currentTime = intent.getLongExtra(IoToolConstants.TimeStampExtra, currentTime);

                syncList = new SyncList(intent.getStringArrayExtra(SyncConstants.SyncListExtra));
                syncProperties = new SyncProperties(serviceName, appDir, sendInterval, liveSession, sessionTimeFrom, sessionTimeTo, session, sendFiles, currentTime);

                boolean permissions = true;
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    if(checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) permissions = false;
                }
                if (!permissions) {
                    Intent i = new Intent();
                    i.setClass(this, IoToolSyncServiceInitialStateCloudPreferences.class);
                    i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    startActivity(i);
                } else {
                    if (sendDataMode) {
                        sendAgain = true;
                        scheduleSend();
                    } else {
                        Toast.makeText(this, getResources().getString(R.string.pref_error_receive_data_not_supported), Toast.LENGTH_SHORT).show();
                        stop(2);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
                stop(4);
                return START_NOT_STICKY;
            }
        }

        // We want this service to continue running until it is explicitly
        // stopped, so return sticky.
        return START_STICKY;
    }

    @Override
    public IBinder onBind(final Intent intent) {
        return null;
    }

    Handler handlerr;

	@Override
	public void onCreate() {
        super.onCreate();
        handlerr = new Handler();
	}


    @Override
    public void onDestroy() {
        super.onDestroy();
        if(commandReceiver!=null)
            unregisterReceiver(commandReceiver);
    }

    public void stop(int cause) {
        serviceRunning = false;
        Log.v("test", "try to stop service "+cause);

        if (sendDataMode) {
            if ((!sendingData) &amp;& (!sendingDataFiles)) {
                Log.v("test", "stopping service");
                sendAgain = false;

                try {
                    sendHandler.removeCallbacks(sendRunnable);
                } catch(Exception e) {
                }

                try {
                    sendFilesHandler.removeCallbacks(sendFilesRunnable);
                } catch(Exception e) {
                }

                // Make sure our notification is gone.
                stopForeground(true);

                stopSelf();
            }
        } else {
            Log.v("test", "stopping service");
            try {
                broadcastHandler.removeCallbacks(broadcastAndReceiveRunnable);
            } catch(Exception e) {
            }
            broadcastAndReceiveRunnable = null;

            // Make sure our notification is gone.
            stopForeground(true);

            stopSelf();
        }
    }

    public static class ConnectServiceCommandReceiver extends BroadcastReceiver {
        private WeakReference<IoToolSyncServiceInitialStateCloud> w;

        public ConnectServiceCommandReceiver(IoToolSyncServiceInitialStateCloud a) {
            w = new WeakReference<IoToolSyncServiceInitialStateCloud>(a);
        }

        public void onReceive(Context context, Intent intent) {
            if(intent==null || !intent.hasExtra(IoToolConstants.ServiceCommandType))
                return;
            IoToolSyncServiceInitialStateCloud a = w.get();

            if(a!=null) {
                if (intent.getStringExtra(IoToolConstants.ServiceCommandType).equals(IoToolConstants.ServiceCommandStopAllServices) ||
                        intent.getStringExtra(IoToolConstants.ServiceCommandType).equals(SyncConstants.CommandStopSyncService)) {
                    a.stop(5);
                } else if (intent.getStringExtra(IoToolConstants.ServiceCommandType).equals(SyncConstants.CommandSetTime)) {
                    if (a.serviceRunning) SyncFunctions.setNewTime(a.syncProperties, a.syncList, intent.getLongExtra(IoToolConstants.TimeStampExtra, 0));
                }
            }
        }
    }

    public void notifyWaiting() {
        if ((!sendingData) && (!sendingDataFiles)) {
            SyncFunctions.notifyWaiting(this, mNM, notification, contentIntent, serviceName, R.string.foreground_service_id);
        }
    }

    public void notifySending() {
        SyncFunctions.notifySending(this, mNM, notification, contentIntent, serviceName, R.string.foreground_service_id);
    }

    public void notifyReceiving() {
        SyncFunctions.notifyReceiving(this, mNM, notification, contentIntent, serviceName, R.string.foreground_service_id);
    }

    public static String parseSendInterval(String sendinterval) {
        String unit = "s";
        if (sendinterval.trim().endsWith("ms")) unit = "ms";
        if (sendinterval.trim().endsWith("m")) unit = "m";
        if (sendinterval.trim().endsWith("h")) unit = "h";
        if (sendinterval.trim().endsWith("d")) unit = "d";
        Integer number;
        try {
            number = Integer.parseInt(sendinterval.replaceAll("[\\D]", ""));
            if (number<1) number = 10;
        } catch (Exception e) {
            number = 10;
        }
        if (unit.equals("ms") && number < 200){
            number = 200; //minimum half second
        }
        sendinterval = number + unit;
        return sendinterval;
    }

    public int getSendIntervalInteger(String sendinterval) {
        Integer number = Integer.parseInt(sendinterval.replaceAll("[\\D]", ""));
        Double numberMs = Double.valueOf(number);
        if (sendinterval.endsWith("m")) number *= 60;
        else if (sendinterval.endsWith("h")) number *= 3600;
        else if (sendinterval.endsWith("d")) number *= 86400;
        return number;
    }

    // ------------------------------ SEND DATA ------------------------------

    public void scheduleSend() {
        if (sendAgain) sendHandler.postDelayed(sendRunnable, syncProperties.sendInterval);
    }

    private static class SendRunnable implements Runnable {
        private WeakReference<IoToolSyncServiceInitialStateCloud> w;

        public SendRunnable(IoToolSyncServiceInitialStateCloud a) {
            w = new WeakReference<IoToolSyncServiceInitialStateCloud>(a);
        }

        public void run() {
            IoToolSyncServiceInitialStateCloud a = w.get();
            if(a!=null) {
                a.sendData();
            }
        }
    }

    public void sendData() {
        if (serviceRunning) {
            Log.v("test", "try to start send data operation");
            SendOperation so = new SendOperation(this);
            so.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
        } else {
            stop(6);
        }
    }

    private static class SendOperation extends AsyncTask<Void, Void, String> {
        private WeakReference<IoToolSyncServiceInitialStateCloud> w;

        public SendOperation(IoToolSyncServiceInitialStateCloud a) {
            w = new WeakReference<IoToolSyncServiceInitialStateCloud>(a);
        }

        @Override
        protected String doInBackground(Void... params) {
            IoToolSyncServiceInitialStateCloud a = w.get();

            if(a!=null) {
                a.sendingData = true;
                a.notifySending();

                SQLiteDatabase dbSync = null;

                try {
                    try{
                        File syncFolder = new File(a.syncProperties.appDir.toString()+"/System");
                        if(!syncFolder.exists()){
                            syncFolder.mkdir();
                        }
                    }catch(Exception ex){
                        ex.printStackTrace();
                    }

                    dbSync = SyncFunctions.getSyncDB(a.syncProperties);

                    Boolean commit = false;

                    Boolean newdata = SyncFunctions.readData(a.syncList, a.syncProperties, dbSync);

                    if (newdata) {
                        commit = a.sendDataToInitialStateCloud(a.syncList);
                    } else {
                        if (a.syncProperties.liveSession) {
                            SyncMonitorFunctions.broadcastMessageWithCurrentTimestamp(a, "No more data found!");
                        } else {
                            if (a.allFilesSent) {
                                a.serviceRunning = false;
                                SyncFunctions.broadcastSessionUploaded(a, a.syncProperties);
                            }
                        }
                    }

                    if (commit) SyncFunctions.markSentData(a.syncList, a.syncProperties, dbSync);

                    dbSync.close();
                } catch (Exception e) {
                    e.printStackTrace();
                    Log.d("IoToolSendData","AsyncTask upload crashed");
                    try {
                        dbSync.close();
                    } catch (Exception e2) {}
                }

                a.sendingData = false;
                a.notifyWaiting();

                if (!a.serviceRunning) {
                    a.stop(7);
                } else {
                    a.scheduleSend();
                }

            }
            return null;
        }
    }

    public boolean sendDataToInitialStateCloud(SyncList syncList) throws IOException {
        if (isInitialStateCloudConnected()) {//if connected, send data to server
            return httpPrepareDataForPublishToInitialStateCloud(syncList);
        }else {
            httpConnectToInitialStateCloud();//connect to server
            if (isInitialStateCloudConnected()) {//if connected, send data to server
                return httpPrepareDataForPublishToInitialStateCloud(syncList);

            }
            return false;
        }
    }

    //connect to Initial State
    private void httpConnectToInitialStateCloud() {
        // TODO get values from preferences
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
        //get parameters from settings
        hostName = sp.getString(getString(R.string.preference_key_hostname),getString(R.string.preference_default_hostname));
        bucketKey = sp.getString(getString(R.string.preference_key_bucket_key),"");
        accessKey = sp.getString(getString(R.string.preference_key_access_key),"");

        sendCondensedData = sp.getBoolean(getString(R.string.preference_key_condensed), false);

        boolean paramsOK = true;
        hostName = hostName.trim();
        if (hostName.length()<1) paramsOK = false;
        if (bucketKey.trim().length()<1) paramsOK = false;
        if (accessKey.trim().length()<1) paramsOK = false;
        if (hostName.charAt(hostName.length()-1)!='/')
            hostName = hostName+"/";


        if (paramsOK) {
            try {
                URL url = new URL(hostName + BASE_DATA_URL);
                client = (HttpsURLConnection) url.openConnection();
                client.setRequestMethod(METHOD_TYPE);
                client.setRequestProperty(CONTENT_TYPE_KEY, CONTENT_TYPE_VALUE);
                client.setRequestProperty(X_ACCESS_KEY, accessKey);
                client.setRequestProperty(ACCEPT_VERSION_KEY, ACCEPT_VERSION_VALUE);
                client.setRequestProperty(BUCKET_KEY, bucketKey);

                client.setDoOutput(true);
                client.setDoInput(true);

                stream = new DataOutputStream(client.getOutputStream());
                isConnected = true;

            }
            catch (MalformedURLException e){
                handlerr.post(new Runnable() {

                    public void run() {
                        if(notShown) {
                            Toast.makeText(IoToolSyncServiceInitialStateCloud.this, "Wrong url", Toast.LENGTH_LONG).show();
                            notShown = false;
                        }
                    }
                });
            }
            catch(UnknownHostException e){
                handlerr.post(new Runnable() {

                    public void run() {
                        if(notShown) {
                            Toast.makeText(IoToolSyncServiceInitialStateCloud.this, "Could not connect", Toast.LENGTH_LONG).show();
                            notShown = false;
                        }
                    }
                });
            }
            catch(IOException e){
                handlerr.post(new Runnable() {

                    public void run() {
                        Toast.makeText(IoToolSyncServiceInitialStateCloud.this,"Could not open output streamer", Toast.LENGTH_LONG).show();
                    }
                });
            }

        } else {
            SyncMonitorFunctions.broadcastMessageWithCurrentTimestamp(getApplication(), getString(R.string.pref_error_enter_params));
            Message msg = handlerToast.obtainMessage();
            msg.arg1 = 1;
            handlerToast.sendMessage(msg);
        }
    }

    //publish to InitialState
    private boolean httpPrepareDataForPublishToInitialStateCloud(final SyncList syncList) {
        //TODO prepare data to send
        LinkedList<Data> data = new LinkedList<Data>();

        for (int i = 0; i < syncList.getCount(); i++) {
            SyncItem syncItem = syncList.getItem(i);
            //if we have data
            if (syncItem.data.size() > 0) {
                if (!sendCondensedData) {//if condensed is turned of, send just last data

                    String[] tmp1 = syncItem.data.get(syncItem.data.size() - 1).split(",");
                    long time = Long.parseLong(tmp1[0]);
                    data.add(new Data(syncItem.getReadingID(),tmp1[1],(time/1000)+"."+tmp1[0].substring(tmp1[0].length()-3)));

                    if (i != syncList.getCount()-1){ //if not last sensor data

                    }else {//get last data from list
                        Data[] test = data.toArray(new Data[data.size()]);
                        return httpPublishDataToInitialStateCloud(test);
                    }
                } else {

                    for (int j = 0; j < syncItem.data.size(); j++) {
                        String[] tmp1 = syncItem.data.get(j).split(",");
                        long time = Long.parseLong(tmp1[0]);
                        data.add(new Data(syncItem.getReadingID(),tmp1[1],(time/1000)+"."+tmp1[0].substring(tmp1[0].length()-3)));
                    }
                }
            }
        }

        if (sendCondensedData && data.size() > 0) { //if condensed sending is turned on, send rest of the payload
            return httpPublishDataToInitialStateCloud(data.toArray(new Data[data.size()]));
        }

        return false;
    }

    private boolean httpPublishDataToInitialStateCloud (Data[] payload){
        //TODO send data to the cloud
        //try{ //send to InitialState
        String body = gson.toJson(payload[0]);
        for(int i = 1;i<payload.length ;i++){
            String tmp = gson.toJson(payload[i]);
            if(body.length()+tmp.length()+3<1000000){
                body = body+","+tmp;
            }
            else{
                try {
                    URL url = new URL(hostName + BASE_DATA_URL);
                    HttpURLConnection client = (HttpsURLConnection) url.openConnection();
                    client.setRequestMethod(METHOD_TYPE);
                    client.setRequestProperty(CONTENT_TYPE_KEY, CONTENT_TYPE_VALUE);
                    client.setRequestProperty(X_ACCESS_KEY, accessKey);
                    client.setRequestProperty(ACCEPT_VERSION_KEY, ACCEPT_VERSION_VALUE);
                    client.setRequestProperty(BUCKET_KEY, bucketKey);

                    client.setDoOutput(true);
                    client.setDoInput(true);

                    DataOutputStream stream = new DataOutputStream(client.getOutputStream());
                    stream.writeBytes("["+body+"]");
                    stream.flush();
                    stream.close();

                    client.disconnect();

                    final int response = client.getResponseCode();
                    if (response == 201) {
                        handlerr.post(new Runnable() {

                            public void run() {
                                Toast.makeText(IoToolSyncServiceInitialStateCloud.this,"New bucket created", Toast.LENGTH_LONG).show();
                            }
                        });
                    } else if (response == 429) {
                        handlerr.post(new Runnable() {

                            public void run() {
                                Toast.makeText(IoToolSyncServiceInitialStateCloud.this,"Your cloud has a limit of 5 requests/s. Some of the information will get lost", Toast.LENGTH_LONG).show();
                            }
                        });
                        return false;
                    } else if(response == -1){
                        handlerr.post(new Runnable() {

                            public void run() {
                                Toast.makeText(IoToolSyncServiceInitialStateCloud.this,"No response from server", Toast.LENGTH_LONG).show();
                            }
                        });
                        return false;
                    } else if (response < 200 || response >= 300) {
                        handlerr.post(new Runnable() {

                            public void run() {
                                Toast.makeText(IoToolSyncServiceInitialStateCloud.this,"Something went wrong: "+response, Toast.LENGTH_LONG).show();
                            }
                        });
                        return false;
                    }
                }
                catch (MalformedURLException e){
                    return false;
                }
                catch(UnknownHostException e){
                    return false;
                }
                catch(IOException e){
                    return false;
                }
                body = tmp;
            }
        }
        try {
            URL url = new URL(hostName + BASE_DATA_URL);
            HttpURLConnection client = (HttpsURLConnection) url.openConnection();
            client.setRequestMethod(METHOD_TYPE);
            client.setRequestProperty(CONTENT_TYPE_KEY, CONTENT_TYPE_VALUE);
            client.setRequestProperty(X_ACCESS_KEY, accessKey);
            client.setRequestProperty(ACCEPT_VERSION_KEY, ACCEPT_VERSION_VALUE);
            client.setRequestProperty(BUCKET_KEY, bucketKey);

            client.setDoOutput(true);
            client.setDoInput(true);

            DataOutputStream stream = new DataOutputStream(client.getOutputStream());
            stream.writeBytes("["+body+"]");
            stream.flush();
            stream.close();

            client.disconnect();

            final int response = client.getResponseCode();
            if (response == 201) {
                handlerr.post(new Runnable() {

                    public void run() {
                        Toast.makeText(IoToolSyncServiceInitialStateCloud.this,"New bucket created", Toast.LENGTH_LONG).show();
                    }
                });
            } else if (response == 429) {
                handlerr.post(new Runnable() {

                    public void run() {
                        Toast.makeText(IoToolSyncServiceInitialStateCloud.this,"Your cloud has a limit of 5 requests/s. Some of the information will get lost", Toast.LENGTH_LONG).show();
                    }
                });
                return false;
            } else if(response == -1){
                handlerr.post(new Runnable() {

                    public void run() {
                        Toast.makeText(IoToolSyncServiceInitialStateCloud.this,"No response from server", Toast.LENGTH_LONG).show();
                    }
                });
                return false;
            } else if (response < 200 || response >= 300) {
                handlerr.post(new Runnable() {

                    public void run() {
                        Toast.makeText(IoToolSyncServiceInitialStateCloud.this,"Something went wrong: "+response, Toast.LENGTH_LONG).show();
                    }
                });
                return false;
            }
        }
        catch (MalformedURLException e){
            return false;
        }
        catch(UnknownHostException e){
            return false;
        }
        catch(IOException e){
            return false;
        }
        return true;

    }

    private boolean isInitialStateCloudConnected() {
        Log.d(getApplication().toString(), ".iSinitialStateCloudConnected() entered");
        boolean connected = false;
        try {
            if ((client != null) && (isConnected)) {
                connected = true;
            }
        } catch (Exception e) {
            // swallowing the exception as it means the client is not connected
        }
        Log.d(getApplication().toString(), ".iSinitialStateCloudConnected() - returning " + connected);
        return connected;
    }

    // ------------------------------ SEND FILES ------------------------------

    public void scheduleSendFiles() {
        if (syncProperties.sendFiles && sendAgain) sendFilesHandler.postDelayed(sendFilesRunnable, syncProperties.sendInterval);
    }

    private static class SendFilesRunnable implements Runnable {
        private WeakReference<IoToolSyncServiceInitialStateCloud> w;

        public SendFilesRunnable(IoToolSyncServiceInitialStateCloud a) {
            w = new WeakReference<IoToolSyncServiceInitialStateCloud>(a);
        }

        public void run() {
            IoToolSyncServiceInitialStateCloud a = w.get();
            if(a!=null) {
                a.sendFiles();
            }
        }
    }

    public void sendFiles() {
        if (serviceRunning) {
            Log.v("test", "try to start send files operation");
            SendFilesOperation sfo = new SendFilesOperation(this);
            //sfo.execute();
            sfo.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
        } else {
            stop(8);
        }
    }

    private static class SendFilesOperation extends AsyncTask<Void, Void, String> {
        private WeakReference<IoToolSyncServiceInitialStateCloud> w;

        public SendFilesOperation(IoToolSyncServiceInitialStateCloud a) {
            w = new WeakReference<IoToolSyncServiceInitialStateCloud>(a);
        }

        @Override
        protected String doInBackground(Void... params) {
            IoToolSyncServiceInitialStateCloud a = w.get();

            if(a!=null) {
                a.sendingDataFiles = true;
                a.notifySending();


                a.sendingDataFiles = false;
                a.notifyWaiting();

                if (!a.serviceRunning) {
                    a.stop(9);
                } else {
                    a.scheduleSendFiles();
                }
            }
            return null;
        }
    }

    // ------------------------------ BROADCAST AND RECEIVE DATA ------------------------------

    public void scheduleBroadcastAndReceive() {
        if (delayBroadcast > 0) {
            delayBroadcast -= broadcastInterval;
            broadcastHandler.postDelayed(broadcastAndReceiveRunnable, 2*broadcastInterval);
        } else broadcastHandler.postDelayed(broadcastAndReceiveRunnable, broadcastInterval);
    }

    private static class BroadcastAndReceiveRunnable implements Runnable {
        private WeakReference<IoToolSyncServiceInitialStateCloud> w;

        public BroadcastAndReceiveRunnable(IoToolSyncServiceInitialStateCloud a) {
            w = new WeakReference<IoToolSyncServiceInitialStateCloud>(a);
        }

        public void run() {
            IoToolSyncServiceInitialStateCloud a = w.get();
            if(a!=null) {
                if (a.serviceRunning) {
                    a.broadcastAndReceiveData();
                } else {
                    a.stop(12);
                }
            }
        }
    }

    public void broadcastAndReceiveData() {
        if (syncList.allDataEmpty()) {
            if (!receivingData) {
                Log.v("test", "no data available for broadcast getting new from server, currentTime="+syncProperties.currentTime);
                SyncMonitorFunctions.broadcastMessageWithCurrentTimestamp(this, "No data available for broadcast getting new from server, selected time = "+SyncMonitorFunctions.timestampToString(syncProperties.currentTime));
                receiveData();
            }
        } else if (!receivingData) {
            Log.v("test", "data available for broadcast, currentTime="+syncProperties.currentTime);
            SyncMonitorFunctions.broadcastMessageWithCurrentTimestamp(this, "Data available for broadcast, time = "+SyncMonitorFunctions.timestampToString(syncProperties.currentTime));

            SyncFunctions.broadcastDataUpToCurrentTime(this, syncProperties, syncList);

            syncProperties.currentTime += broadcastInterval;
        }

        scheduleBroadcastAndReceive();
    }

    // ------------------------------ RECEIVE DATA ------------------------------

    public void receiveData() {
        if (serviceRunning) {
            ReceiveOperation ro = new ReceiveOperation(this);
            ro.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
        } else {
            stop(13);
        }
    }

    private static class ReceiveOperation extends AsyncTask<Void, Void, String> {
        private WeakReference<IoToolSyncServiceInitialStateCloud> w;

        public ReceiveOperation(IoToolSyncServiceInitialStateCloud a) {
            w = new WeakReference<IoToolSyncServiceInitialStateCloud>(a);
        }

        @Override
        protected String doInBackground(Void... params) {
            IoToolSyncServiceInitialStateCloud a = w.get();
            if(a!=null) {
                a.notifyReceiving();
                a.receivingData = true;

                a.receivingData = false;
                a.notifyWaiting();

                if (!a.serviceRunning) {
                    a.stop(11);
                }
            }
            return null;
        }
    }
}

"IoToolSyncService"+ serviceID +"IoTPreferences.java"

This class implements preferences for a sync extension which are accessible from IoTool app. Any additional preferences could be added or removed. These are usually related to keys and parameters that relate to connection to the cloud of your choosing.

package io.senlab.iotool.extension.initialstatecloud;

import android.Manifest;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.preference.CheckBoxPreference;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceManager;
import android.util.Log;
import android.widget.Toast;

import java.lang.ref.WeakReference;

import io.senlab.iotool.library.base.AppCompatPreferenceActivity;
import io.senlab.iotool.library.base.IoToolConstants;
import io.senlab.iotool.library.base.IoToolFunctions;
import io.senlab.iotool.library.sync.SyncFunctions;

public class IoToolSyncServiceInitialStateCloudPreferences extends AppCompatPreferenceActivity {

	private PreferenceManager pm;

	private EditTextPreference SendInterval;

	private EditTextPreference hostname;
	private EditTextPreference username;
	private EditTextPreference password;

	@Override
    public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		getSupportActionBar().setTitle(SyncFunctions.getSyncServiceFullName(this, IoToolSyncServiceInitialStateCloud.serviceName, getPackageName()));

	    // Load the preferences from an XML resource
	    addPreferencesFromResource(R.xml.datatransferservicepreferences);

	    pm = getPreferenceManager();

	    initPreferences();

		SendInterval.setOnPreferenceChangeListener(new OnSendIntervalPrefChangeListener(this));

		loadSettings();
	}

	public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
		switch (requestCode) {
			case IoToolConstants.PERMISSIONS_REQUEST_CODE: {
				boolean alert = false;
				// If request is cancelled, the result arrays are empty.
				if (grantResults.length > 0) {
					for (int grantResult : grantResults) {
						if (grantResult != PackageManager.PERMISSION_GRANTED) alert = true;
					}
				} else alert = true;
				if (alert) {
					Toast.makeText(getApplicationContext(), "DataTransfer"+" "+getString(R.string.request_permission_explanation), Toast.LENGTH_LONG).show();
					finish();
				}
				return;
			}
		}

	}

	@Override
	public void onResume() {
		super.onResume();

		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
			boolean requestPermissions = false;
			if(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) requestPermissions = true;
			if(requestPermissions) {
				//Need to delay this so the UI can be displayed all the way
				(new Handler()).postDelayed(new Runnable() {
					@Override
					public void run() {
						if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
							requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, IoToolConstants.PERMISSIONS_REQUEST_CODE);
						}
					}
				}, IoToolConstants.PERMISSIONS_DIALOG_DISPLAY_TIME_MILLIS);
			}
		}
	}

    @Override
	public void onAttachedToWindow() {
    	super.onAttachedToWindow();
    	IoToolFunctions.onAttachedToWindow(this);
	}

	@Override
	public void onBackPressed() {
		super.onBackPressed();
		saveSettings();
	}

	@Override
	public void onDestroy() {
		super.onDestroy();
		new SaveSettingsInBackground(this).execute();
	}

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
    	super.onConfigurationChanged(newConfig);
    }

	private void initPreferences() {
		//TODO initialize objects to manipulate preference values
		SendInterval = (EditTextPreference) pm.findPreference(getString(R.string.preference_key_sendinterval));

		hostname = (EditTextPreference)pm.findPreference(getString(R.string.preference_key_hostname));
		username = (EditTextPreference)pm.findPreference(getString(R.string.preference_key_access_key));
		password = (EditTextPreference)pm.findPreference(getString(R.string.preference_key_bucket_key));


	}

	private void loadSettings() {
    }

	private void saveSettings() {
		//TODO change values before saving
    	try {

			if(SendInterval.getText().toString().toUpperCase().equals(""))
				SendInterval.setText(getString(R.string.preference_default_sendinterval));

			if(hostname.getText().toString().toUpperCase().equals(""))
				SendInterval.setText(getString(R.string.preference_default_hostname));

    		System.gc();
		} catch (Exception e) {
			e.printStackTrace();
			Log.e("IoTool Log", "Error stack trace on save:" +e.getMessage());
		}
    }

    private static class SaveSettingsInBackground extends AsyncTask<Void, Void, Void> {
		private WeakReference<IoToolSyncServiceInitialStateCloudPreferences> w;

		public SaveSettingsInBackground(IoToolSyncServiceInitialStateCloudPreferences a) {
			w = new WeakReference<IoToolSyncServiceInitialStateCloudPreferences>(a);
		}
		protected Void doInBackground(Void... params) {
			IoToolSyncServiceInitialStateCloudPreferences a = w.get();

			if(a!=null) {
				a.saveSettings();
			}
			return null;
		}
    }

	private static class OnSendIntervalPrefChangeListener implements Preference.OnPreferenceChangeListener {
		private WeakReference<IoToolSyncServiceInitialStateCloudPreferences> w;

		public OnSendIntervalPrefChangeListener(IoToolSyncServiceInitialStateCloudPreferences a) {
			w = new WeakReference<IoToolSyncServiceInitialStateCloudPreferences>(a);
		}

		public boolean onPreferenceChange(Preference preference, Object newValue) {
			IoToolSyncServiceInitialStateCloudPreferences a = w.get();

			if(a!=null) {
				((EditTextPreference) preference).setText(IoToolSyncServiceInitialStateCloud.parseSendInterval(newValue.toString()));
			}

			return false;
		}
	}

	private static class OnDatabaseUserPrefChangeListener implements Preference.OnPreferenceChangeListener {
		private WeakReference<IoToolSyncServiceInitialStateCloudPreferences> w;

		public OnDatabaseUserPrefChangeListener(IoToolSyncServiceInitialStateCloudPreferences a) {
			w = new WeakReference<IoToolSyncServiceInitialStateCloudPreferences>(a);
		}

		public boolean onPreferenceChange(Preference preference, Object newValue) {
			IoToolSyncServiceInitialStateCloudPreferences a = w.get();

			if(a!=null) {
				String email = newValue.toString();
				if (IoToolFunctions.isValidEmail(email)) return true;
				else {
					Toast.makeText(a.getApplicationContext(), a.getString(R.string.invalid_database_username), Toast.LENGTH_SHORT).show();
					return false;
				}
			} else {
				return false;
			}
		}
	}
}

"IoToolSyncService"+ serviceID +"IoTProvider.java"

It must provide info for sync properties. Info is stored in two arrays, one for headers and one for actual data.

Properties:

  • IoToolConstants.PROVIDER_ROWID - unique row id (integer)

  • IoToolConstants.PROVIDER_PROFILES_NAME - profile name

  • IoToolConstants.PROVIDER_PROFILES_DEVELOPER - developer name

package io.senlab.iotool.extension.initialstatecloud;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.SQLException;
import android.net.Uri;

import io.senlab.iotool.library.base.IoToolConstants;

public class IoToolSyncServiceInitialStateCloudProvider extends ContentProvider {
	//TODO setting provider name and properties
	static final String PROVIDER_NAME = "io.senlab.iotool.extension.initialstatecloud.IoToolSyncServiceInitialStateCloudProvider";

	static final String[] properties_header = new String[] {IoToolConstants.PROVIDER_ROWID, IoToolConstants.PROVIDER_PROPERTIES_NAME, IoToolConstants.PROVIDER_PROPERTIES_DEVELOPER};
	static final Object[] properties = new Object[] {1, "Initial State Cloud", "SenLab"};

	static final int PROPERTIES = 1;

	static final UriMatcher uriMatcher;
	static{
		uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
		uriMatcher.addURI(PROVIDER_NAME, "properties", PROPERTIES);
	}

	@Override
	public boolean onCreate() {
		return true;
   	}

	@Override
	public Uri insert(Uri uri, ContentValues values) {
		throw new SQLException("Failed to add a record into " + uri);
	}

	@Override
	public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
		MatrixCursor mc = null;
		switch (uriMatcher.match(uri)){
			case PROPERTIES:
				mc = new MatrixCursor(properties_header);
				mc.addRow(properties);
				break;
			default:
				throw new IllegalArgumentException("Unknown URI " + uri);
		}
		return mc;
	}

	@Override
	public int delete(Uri uri, String selection, String[] selectionArgs) {
		return 0;
	}

	@Override
	public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
		return 0;
	}

	@Override
	public String getType(Uri uri) {
		switch (uriMatcher.match(uri)){
			case PROPERTIES:
				return "vnd.android.cursor.dir/vnd.iotool.properties";
			default:
				throw new IllegalArgumentException("Unsupported URI: " + uri);
		}
	}
}