com.raceyourself.android.samsung.ProviderService.java Source code

Java tutorial

Introduction

Here is the source code for com.raceyourself.android.samsung.ProviderService.java

Source

/**
 * Copyright (c) 2014 RaceYourself Inc
 * All Rights Reserved
 *
 * No part of this application or any of its contents may be reproduced, copied, modified or 
 * adapted, without the prior written consent of the author, unless otherwise indicated.
 *
 * Commercial use and distribution of the application or any part is not allowed without 
 * express and prior written consent of the author.
 *
 * The application makes use of some publicly available libraries, some of which have their 
 * own copyright notices and licences. These notices are reproduced in the Open Source License 
 * Acknowledgement file included with this software.
 * 
 */

package com.raceyourself.android.samsung;

import java.io.IOException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

import org.json.JSONException;
import org.json.JSONObject;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.AlertDialog;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.util.Base64;
import android.util.Log;
import android.view.WindowManager;

import com.glassfitgames.glassfitplatform.gpstracker.GPSTracker;
import com.glassfitgames.glassfitplatform.gpstracker.Helper;
import com.glassfitgames.glassfitplatform.gpstracker.SyncHelper;
import com.glassfitgames.glassfitplatform.models.Device;
import com.glassfitgames.glassfitplatform.models.Preference;
import com.glassfitgames.glassfitplatform.models.RemoteConfiguration;
import com.glassfitgames.glassfitplatform.models.UserDetail;
import com.google.android.gms.plus.PlusShare;
import com.raceyourself.android.samsung.models.GpsPositionData;
import com.raceyourself.android.samsung.models.GpsStatusResp;
import com.raceyourself.android.samsung.models.RemoteConfigurationResp;
import com.raceyourself.android.samsung.models.SAModel;
import com.raceyourself.android.samsung.models.ShareScoreReq;
import com.raceyourself.android.samsung.models.WebLinkReq;
import com.raceyourself.samsungprovider.R;
import com.roscopeco.ormdroid.ORMDroidApplication;
import com.samsung.android.sdk.accessory.SAAgent;
import com.samsung.android.sdk.accessory.SAPeerAgent;
import com.samsung.android.sdk.accessory.SASocket;

public class ProviderService extends SAAgent {

    public static final String TAG = "RaceYourselfProvider";
    public static final String CONSUMER_TAG = "RaceYourselfConsumer";
    public final int DEFAULT_CHANNEL_ID = 104;
    private final String SERVER_TOKEN = "3hrJfCEZwQbACyUB";
    private static final String EULA_KEY = "EulaAccept";
    private static final String DISCLAIMER_KEY = "DisclaimerAccept";
    private static final String SUCCESS_KEY = "SuccessAccept";

    private final IBinder mBinder = new LocalBinder();
    private static HashMap<Integer, RaceYourselfSamsungProviderConnection> mConnectionsMap = new HashMap<Integer, RaceYourselfSamsungProviderConnection>();
    private static GPSTracker gpsTracker = null;
    private static GpsDataSender gpsDataSender = null;
    private Timer timer = new Timer();

    private AlertDialog alert;
    private AlertDialog waitingAlert;
    private boolean initialisingInProgress = false;

    private boolean registered = false; // have we registered the device with the server yet? Required for inserting stuff into the db.
    private Thread deviceRegistration = null;

    private final int TETHER_NOTIFICATION_ID = 1;
    private boolean iconEnabled = false;
    private long lastGpsReqTimestamp = 0; // the last time we were asked for a gps status

    public class LocalBinder extends Binder {
        public ProviderService getService() {
            return ProviderService.this;
        }
    }

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

    @Override
    public boolean onUnbind(Intent intent) {
        stopTracking();
        return false;
    }

    /**
     *  Called when the service is started, even if already running
     */
    @Override
    public int onStartCommand(Intent i, int j, int k) {

        ORMDroidApplication.initialize(this); // init the database

        // check the user has done everything they need to
        // separate thread to allow onCreate() to complete and SAP to 
        // deploy the wgt to gear.
        // Don't trigger if init is already in progress, or we're already connected to gear
        if (!initialisingInProgress && mConnectionsMap.isEmpty()) {
            new Handler().post(new Runnable() {
                public void run() {
                    runUserInit();
                }
            });
        }
        // make sure we have a record for the user
        Helper.getUser();
        // Initialize helper singleton
        Helper.getInstance(this);

        int result = super.onStartCommand(i, j, k);

        Log.v(TAG, "service created");

        return result;
    }

    public void runUserInit() {
        Log.d(TAG, "Running user init");
        initialisingInProgress = true;

        // do in background
        if (registered) {
            authorize();
            trySync();
        }

        // check EULA
        Boolean eulaAccept = Preference.getBoolean(EULA_KEY);
        if (eulaAccept == null || !eulaAccept.booleanValue()) {
            popupEula();
            return;
        }

        // register with server
        if (!ensureDeviceIsRegistered()) {
            popupNetworkDialog();
            return;
        }

        // check bluetooth - if they want to launch the app now
        if (!Helper.getInstance(this).isBluetoothBonded()) {
            popupBluetoothDialog();
            return;
        }

        // check the gear app is running

        if (mConnectionsMap.isEmpty()) {
            popupWaitingForGearDialog();
            return;
        }

        Log.d(TAG, "User init completed successfully");
        initialisingInProgress = false;

        // do in background
        if (registered) {
            authorize();
            trySync();
        }
    }

    @Override
    public void onLowMemory() {
        Log.e(TAG, "onLowMemory  has been hit better to do  graceful  exit now");
        //      Toast.makeText(getBaseContext(), "!!!onLowMemory!!!", Toast.LENGTH_LONG)
        //      .show();
        closeConnection();
        super.onLowMemory();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Helper.getInstance(this).destroy(); // unregister intent receivers etc
        Log.i(TAG, "Service Stopped.");
    }

    public ProviderService() {
        super(TAG, RaceYourselfSamsungProviderConnection.class);
    }

    public boolean closeConnection() {

        Log.d(TAG, "closeConnection called");

        if (mConnectionsMap != null) {
            List<Integer> listConnections = new ArrayList<Integer>(mConnectionsMap.keySet());
            if (listConnections != null) {
                for (Integer s : listConnections) {
                    Log.i(TAG, "KEYS found are" + s);
                    mConnectionsMap.get(s).close();
                    mConnectionsMap.remove(s);
                }

                // if no connections left
                if (mConnectionsMap.isEmpty()) {
                    // stop sending updates
                    Log.d(TAG, "No connections remaining, destroying GPS tracker");
                    stopTracking();
                }
            }
        } else
            Log.e(TAG, "mConnectionsMap is null");

        return true;
    }

    /**
     * 
     * @param uThisConnection
     * @param result
     * 
     * Based on Samsung sample app by s.amit
     * 
     */
    @Override
    protected void onServiceConnectionResponse(SASocket uThisConnection, int result) {
        if (result == CONNECTION_SUCCESS) {
            if (uThisConnection != null) {

                RaceYourselfSamsungProviderConnection myConnection = (RaceYourselfSamsungProviderConnection) uThisConnection;
                Log.d(TAG, "onServiceConnection connectionID = " + myConnection.mConnectionId);

                myConnection.mConnectionId = (int) (System.currentTimeMillis() & 255);
                mConnectionsMap.put(myConnection.mConnectionId, myConnection);

                // Enabled 'tethered' icon
                enableIcon();

                // make sure we have a device ID (initially from the server) - req for writes to db
                ensureDeviceIsRegistered();

                // init GPS tracker to start searching for position
                ensureGps();

                try {
                    waitingAlert.cancel();
                } catch (Exception e) {
                    // waiting Alert may have been null, or not popped up,
                    // in which case don't worry
                }

                try {
                    alert.cancel();
                } catch (Exception e) {
                    // waiting Alert may have been null, or not popped up,
                    // in which case don't worry
                }

                Boolean successAccept = Preference.getBoolean(SUCCESS_KEY);
                if (successAccept == null || !successAccept.booleanValue()) {
                    popupSuccessDialog();
                }

                Log.e(TAG, "Successfully connected to Gear");

            } else
                Log.e(TAG, "SASocket object is null");
        } else
            Log.e(TAG, "onServiceConnectionResponse result error =" + result);

    }

    /**
     * 
     * @param connectedPeerId
     * @param channelId
     * @param data
     */
    private void onDataAvailableOnChannel(String connectedPeerId, long channelId, String data) {

        Log.d(TAG, "Received message on channel " + channelId + " from peer " + connectedPeerId + ": " + data);

        SAModel response = null;

        ensureDeviceIsRegistered();

        // decide what to do based on the message
        if (data.contains(SAModel.GPS_STATUS_REQ)) {
            ensureGps();
            if (gpsTracker.hasPosition() && registered) {
                response = new GpsStatusResp(GpsStatusResp.GPS_READY);
            } else if (gpsTracker.isGpsEnabled()) {
                response = new GpsStatusResp(GpsStatusResp.GPS_ENABLED);
            } else {
                response = new GpsStatusResp(GpsStatusResp.GPS_DISABLED);
                if (System.currentTimeMillis() > lastGpsReqTimestamp + 3000) {
                    popupGpsDialog(); // only popup on first poll (they happen every second at time of writing..)
                }
                lastGpsReqTimestamp = System.currentTimeMillis();
            }
        } else if (data.contains(SAModel.START_TRACKING_REQ)) {
            startTracking();
        } else if (data.contains(SAModel.STOP_TRACKING_REQ)) {
            stopTracking();
        } else if (data.contains(SAModel.AUTHENTICATION_REQ)) {
            // usually called when the user launches the app on gear
            authorize();
            trySync();
            // Popup webview with user/passwd boxes
            //Intent authenticationIntent = new Intent (this, AuthenticationActivity.class);
            //authenticationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            //authenticationIntent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
            //startActivity(authenticationIntent);
        } else if (data.contains(SAModel.LOG_ANALYTICS)) {
            try {
                JSONObject json = new JSONObject(data);
                Helper.logEvent(json.getString("value"));
            } catch (JSONException e) {
                Log.e(TAG, "Error parsing analytics event", e);
            }
        } else if (data.contains(SAModel.LOG_TO_ADB)) {
            try {
                JSONObject json = new JSONObject(data);
                String logLevel = json.getString("logLevel");
                String logMessage = json.getString("logMessage");
                if (logLevel.equals("VERBOSE"))
                    Log.v(CONSUMER_TAG, logMessage);
                else if (logLevel.equals("DEBUG"))
                    Log.d(CONSUMER_TAG, logMessage);
                else if (logLevel.equals("INFO"))
                    Log.i(CONSUMER_TAG, logMessage);
                else if (logLevel.equals("WARNING"))
                    Log.w(CONSUMER_TAG, logMessage);
                else if (logLevel.equals("ERROR"))
                    Log.e(CONSUMER_TAG, logMessage);
            } catch (JSONException e) {
                Log.e(TAG, "Error parsing sap-to-adb message", e);
            }
        } else if (data.contains(SAModel.WEB_LINK_REQ)) {
            JSONObject json;
            try {
                json = new JSONObject(data);
                String uri = WebLinkReq.fromJSON(json).getUri();
                launchWebBrowser(uri);
            } catch (JSONException e) {
                Log.e(TAG, "Error parsing WebLinkReq", e);
            }
        } else if (data.contains(SAModel.REMOTE_CONFIGURATION_REQ)) {
            RemoteConfiguration config = SyncHelper.get("configurations/gear", RemoteConfiguration.class);
            if (config != null)
                response = new RemoteConfigurationResp(config.configuration);
        } else if (data.contains(SAModel.SHARE_SCORE_REQ)) {
            ShareScoreReq req = null;
            try {
                req = ShareScoreReq.fromJSON(new JSONObject(data));
            } catch (JSONException e) {
                Log.e(TAG, "Error parsing ShareHighscoreReq", e);
            }

            if (req != null) {
                String filenameFormat = "gear-score-%d-%d-%s";
                String shareType = "score";
                if (req.getHighscore() < req.getScore()) {
                    filenameFormat = "gear-high-score-%d-%d-%s";
                    shareType = "highscore";
                }

                String imageUrl = "http://shared.raceyourself.com/" + MD5(String.format(filenameFormat,
                        req.getScore(), Math.max(req.getScore(), req.getHighscore()), req.getSubtext())) + ".jpg";

                Helper.logEvent(
                        String.format("{\"event_type\":\"share\", \"share_type\":\"%s\", \"service\":\"%s\"}",
                                shareType, req.getScore()));
                if ("google+".equals(req.getService())) {
                    String text = String.format(req.getScore() > 1 ? "I ran %d laps!" : "I ran %d lap",
                            req.getScore());
                    if (req.getHighscore() < req.getScore())
                        text = "I got a new highscore! " + text;
                    Intent shareIntent = new PlusShare.Builder(this).setType("text/plain")
                            .setText(text + " #RaceYourself").setContentUrl(Uri.parse(imageUrl)).getIntent();

                    shareIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    startActivity(shareIntent);
                } else if ("facebook".equals(req.getService())) {
                    Intent customIntent = new Intent("com.raceyourself.intent.FACEBOOK_SHARE");
                    if (req.getHighscore() < req.getScore())
                        customIntent.putExtra("name", "New Highscore!");
                    else
                        customIntent.putExtra("name", "Eliminated!");
                    customIntent.putExtra("caption", String.format(req.getScore() > 1 || req.getScore() == 0
                            ? "I survived %d laps of the Eliminator! Get the app free at http://www.raceyourself.com"
                            : "I survived %d lap of the Eliminator! Get the app free at http://www.raceyourself.com",
                            req.getScore()));
                    //                    customIntent.putExtra("description", "");
                    customIntent.putExtra("picture", imageUrl);
                    customIntent.putExtra("link", "https://www.raceyourself.com/gear");

                    customIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    startActivity(customIntent);
                } else if ("twitter".equals(req.getService())) {
                    String cardUrl = "http://shared.raceyourself.com/" + MD5(String.format(filenameFormat,
                            req.getScore(), Math.max(req.getScore(), req.getHighscore()), req.getSubtext()))
                            + ".html";
                    String text = String.format(
                            req.getScore() > 1 || req.getScore() == 0 ? "I survived %d laps of the Eliminator!"
                                    : "I survived %d lap of the Eliminator!",
                            req.getScore());
                    String url = String.format("https://twitter.com/intent/tweet?url=%s&text=%s&via=Race_Yourself",
                            cardUrl, text);
                    Intent shareIntent = new Intent(Intent.ACTION_VIEW);
                    shareIntent.setData(Uri.parse(url));

                    shareIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    startActivity(shareIntent);
                } else {
                    Log.e(TAG, "onDataAvailableOnChannel: ShareHighScoreReq: Unknown service: " + req.getService());
                }
            }
        } else {
            Log.e(TAG, "onDataAvailableOnChannel: Unknown request received");
        }

        // send the response
        if (response != null) {
            send(connectedPeerId, response);
        }
    }

    private void popupBluetoothDialog() {
        final AlertDialog.Builder builder = new AlertDialog.Builder(this);

        builder.setMessage("Your bluetooth is not enabled.\n\nPlease enable, connect to Gear and press retry.")
                .setCancelable(false).setPositiveButton("Retry", new DialogInterface.OnClickListener() {

                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        //Intent bluetooth = new Intent(android.provider.Settings.ACTION_BLUETOOTH_SETTINGS);
                        //bluetooth.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                        //startActivity(bluetooth);

                        // continue with init
                        runUserInit();
                    }
                }).setNegativeButton("Quit", new DialogInterface.OnClickListener() {

                    @Override
                    public void onClick(final DialogInterface dialog, @SuppressWarnings("unused") final int id) {
                        // TODO Auto-generated method stub
                        dialog.cancel();
                    }
                }).setTitle("RaceYourself Gear Edition");

        alert = builder.create();
        alert.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
        alert.show();
    }

    private void popupGpsDialog() {
        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setMessage("RaceYourself works best with GPS.\n\nWould you like to enable your GPS now?")
                .setCancelable(false).setPositiveButton("Yes", new DialogInterface.OnClickListener() {
                    public void onClick(@SuppressWarnings("unused") final DialogInterface dialog,
                            @SuppressWarnings("unused") final int id) {
                        Intent gps = new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS);
                        gps.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                        startActivity(gps);
                    }
                }).setNegativeButton("No", new DialogInterface.OnClickListener() {
                    public void onClick(final DialogInterface dialog, @SuppressWarnings("unused") final int id) {
                        dialog.cancel();
                    }
                }).setTitle("RaceYourself Gear Edition");
        alert = builder.create();
        alert.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
        alert.show();
    }

    private void popupEula() {

        final String message = new String(
                "End-user license agreement and disclaimer.\n\nBy clicking accept you agree to abide by RaceYourself's terms and conditions of use. You also agree to take full responsibility for you own safety whilst using RaceYourself, and accept that RaceYourself will not be held liable for any personal injury or illness sustained through use of this application.\n\nYou agree to the full EULA and Disclaimers which can be viewed online:\n\nhttp://www.raceyourself.com/gear/#eula\n\nhttp://www.raceyourself.com/gear/#disclaimer");
        //Linkify.addLinks(message, Linkify.WEB_URLS);

        //final TextView view = new TextView(this);
        //view.setText(message);
        //view.setMovementMethod(LinkMovementMethod.getInstance());
        //view.setOnClickListener(new OnClickListener() {
        //    public void onClick(View onClick) {                 
        //        launchWebBrowser("http://www.raceyourself.com/gear/#eula");
        //    }
        //});

        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setMessage(message).setCancelable(true)
                .setPositiveButton("Agree", new DialogInterface.OnClickListener() {
                    public void onClick(@SuppressWarnings("unused") final DialogInterface dialog,
                            @SuppressWarnings("unused") final int id) {
                        Preference.setBoolean(EULA_KEY, Boolean.TRUE);
                        Helper.logEvent(
                                "{\"event_type\":\"Progress guard\", \"guard\":\"GearProvider EULA\", \"passed\":true}");

                        // continue with init
                        new Handler().post(new Runnable() {
                            public void run() {
                                runUserInit();
                            }
                        });
                    }
                })
                /* The following doesn't allow synchronous return to the dialog. Probably need an activity here.
                 * TODO: fix this so the link is clickable
                 * .setNeutralButton("Details", new DialogInterface.OnClickListener() {
                    public void onClick(final DialogInterface dialog, @SuppressWarnings("unused") final int id) {
                         // link to website
                        launchWebBrowser("http://www.raceyourself.com/gear/#eula");
                    }
                })*/
                .setNegativeButton("Decline", new DialogInterface.OnClickListener() {
                    public void onClick(final DialogInterface dialog, @SuppressWarnings("unused") final int id) {
                        ProviderService.this.stopSelf();
                    }
                }).setTitle("RaceYourself Gear Edition");
        alert = builder.create();
        alert.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
        alert.show();
    }

    private void popupDisclaimer() {
        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("RaceYourself Gear Edition").setMessage(
                "Disclaimer\n\nBy clicking accept you agree to take full responsibility for you own safety whilst using RaceYourself, and accept that RaceYourself will not be held liable for any personal injury or illness sustained through use of this application.\n\nYou agree to the full RaceYourself disclaimer which can be viewed online at https://www.raceyourself.com/gear/#disclaimer")
                .setCancelable(false).setPositiveButton("Accept", new DialogInterface.OnClickListener() {
                    public void onClick(@SuppressWarnings("unused") final DialogInterface dialog,
                            @SuppressWarnings("unused") final int id) {
                        Preference.setBoolean(DISCLAIMER_KEY, Boolean.TRUE);
                        Helper.logEvent(
                                "{\"event_type\":\"Progress guard\", \"guard\":\"GearProvider Disclaimer\", \"passed\":true}");

                        // continue with init
                        new Handler().post(new Runnable() {
                            public void run() {
                                runUserInit();
                            }
                        });
                    }
                }).setNegativeButton("Decline", new DialogInterface.OnClickListener() {
                    public void onClick(final DialogInterface dialog, @SuppressWarnings("unused") final int id) {
                        ProviderService.this.stopSelf();
                    }
                });
        alert = builder.create();
        alert.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
        alert.show();
    }

    private void popupNetworkDialog() {
        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setMessage(
                "RaceYourself needs to connect to the internet to register your device. Please check your connection and press retry.")
                .setCancelable(false).setPositiveButton("Retry", new DialogInterface.OnClickListener() {
                    public void onClick(@SuppressWarnings("unused") final DialogInterface dialog,
                            @SuppressWarnings("unused") final int id) {
                        runUserInit();
                    }
                }).setNegativeButton("Quit", new DialogInterface.OnClickListener() {
                    public void onClick(final DialogInterface dialog, @SuppressWarnings("unused") final int id) {
                        ProviderService.this.stopSelf();
                    }
                }).setTitle("RaceYourself Gear Edition");
        alert = builder.create();
        alert.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
        alert.show();
    }

    private void popupWaitingForGearDialog() {
        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("RaceYourself Gear Edition").setMessage(
                "Please launch RaceYourself on Gear.\n\nIf you have just installed RaceYourself, the icon may take a few moments to appear. \n\nIf you are waiting a while, make sure Gear Manager shows a connection to gear or try disabling/re-enabing bluetooth on Gear.")
                .setCancelable(false).setPositiveButton("Retry", new DialogInterface.OnClickListener() {
                    public void onClick(@SuppressWarnings("unused") final DialogInterface dialog,
                            @SuppressWarnings("unused") final int id) {
                        new Handler().post(new Runnable() {
                            public void run() {
                                runUserInit();
                            }
                        });
                    }
                }).setNegativeButton("Quit", new DialogInterface.OnClickListener() {
                    public void onClick(final DialogInterface dialog, @SuppressWarnings("unused") final int id) {
                        ProviderService.this.stopSelf();
                    }
                });
        waitingAlert = builder.create();
        waitingAlert.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
        waitingAlert.show();
    }

    private void popupSuccessDialog() {
        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("RaceYourself Gear Edition").setMessage(
                "Connected to Gear.\n\nRaceYourself is ready to go!\n\nIt's important to keep Gear connected to your phone when you workout, so RaceYourself can use the GPS in your phone.")
                .setCancelable(false).setPositiveButton("Ok", new DialogInterface.OnClickListener() {
                    public void onClick(@SuppressWarnings("unused") final DialogInterface dialog,
                            @SuppressWarnings("unused") final int id) {
                        Preference.setBoolean(SUCCESS_KEY, Boolean.TRUE);
                    }
                }).setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                    public void onClick(final DialogInterface dialog, @SuppressWarnings("unused") final int id) {
                        // nothing - just dismiss dialog
                    }
                });
        alert = builder.create();
        alert.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
        alert.show();
    }

    private void send(String connectedPeerId, SAModel message) {
        RaceYourselfSamsungProviderConnection conn = mConnectionsMap.get(Integer.parseInt(connectedPeerId));
        try {
            Log.d(TAG, "Sending message on channel " + DEFAULT_CHANNEL_ID + ": " + message.toJSON().toString());
            conn.send(DEFAULT_CHANNEL_ID, message.toJSON().toString().getBytes());
        } catch (IllegalArgumentException e) {
            Log.e(TAG, "IllegalArgumentException sending SAP message", e);
        } catch (IOException e) {
            Log.e(TAG, "IOException sending SAP message", e);
        } catch (JSONException e) {
            Log.e(TAG, "JSONException sending SAP message", e);
        }
    }

    private boolean ensureDeviceIsRegistered() {
        if (registered) {
            // registered, and we know about it locally
            return true;
        } else if (Device.self() != null) {
            // registered previously, update local variable
            registered = true;
            return true;
        } else if (Helper.getInstance(this).hasInternet()) {
            try {
                Device self = SyncHelper.registerDevice();
                self.self = true;
                self.save();
                registered = true;

                authorize();
                trySync();
                return true;

            } catch (IOException e) {
                Log.e(TAG, "Error registering device", e);
                return false;
            }
        } else {
            // not yet registered, no internet, need to prompt user
            this.popupNetworkDialog();
            return false;
        }
    }

    private void ensureGps() {
        // start listening for GPS updates
        Log.d(TAG, "ensureGps called");
        if (gpsTracker == null)
            gpsTracker = new GPSTracker(this);
        gpsTracker.setIndoorMode(false);
        gpsTracker.onResume();
    }

    public boolean hasBluetooth() {
        return Helper.getInstance(this).isBluetoothBonded();
    }

    public boolean hasGear() {
        return !mConnectionsMap.isEmpty();
    }

    private void startTracking() {

        Log.d(TAG, "startTracking called");
        ensureGps();
        if (!ensureDeviceIsRegistered()) {
            Log.e(TAG, "startTracking called before device was registered");
            return;
        }
        gpsTracker.startTracking();

        if (gpsDataSender == null) {
            Log.d(TAG, "Starting to send regular GPS data messages");
            gpsDataSender = new GpsDataSender();
            timer.scheduleAtFixedRate(gpsDataSender, 0, 500);
        }
    }

    private void stopTracking() {

        // stop sending updates
        Log.d(TAG, "stopTracking called");
        if (gpsDataSender != null) {
            Log.d(TAG, "Stopping regular GPS data messages");
            gpsDataSender.cancel();
            gpsDataSender = null;
        }

        // stop listening for GPS/sensors
        if (gpsTracker != null) {
            gpsTracker.stopTracking();
            gpsTracker.onPause();
        }
        trySync(); // need to sync every now and then. End of each race seems reasonable. It runs in a background thread.
    }

    private void ensureInternet() {
        if (!Helper.getInstance(this).hasInternet()) {
            popupNetworkDialog();
        }
    }

    private void authorize() {
        UserDetail me = Helper.getUser();
        if (me != null && me.getApiAccessToken() != null)
            return;

        AccountManager mAccountManager = AccountManager.get(this);
        List<Account> accounts = new ArrayList<Account>();
        accounts.addAll(Arrays.asList(mAccountManager.getAccountsByType("com.google")));
        accounts.addAll(Arrays.asList(mAccountManager.getAccountsByType("com.googlemail")));
        String email = null;
        for (Account account : accounts) {
            if (account.name != null && account.name.contains("@")) {
                email = account.name;
                break;
            }
        }
        // Potential fault: Can there be multiple accounts? Do we need to sort or provide a selector?

        // hash email so we don't send user's identity to server
        // can't guarantee uniqueness but want very low probability of collisions
        // using SHA-256 means we'd expect a collision on approx. our 1-millionth user
        // TODO: make this more unique before Samsung sell 1m Gear IIs.
        MessageDigest messageDigest;
        try {
            messageDigest = MessageDigest.getInstance("SHA-256");
            messageDigest.update(email.getBytes());
            String hash = new String(messageDigest.digest());
            hash = new String(Base64.encode(hash.getBytes(), Base64.DEFAULT)).replace("@", "_").replace("\n", "_"); //base64 encode and substitute @ symbols
            hash += "@hashed.raceyourself.com"; // make it look like an email so it passes server validation
            Helper.login(hash, SERVER_TOKEN);
        } catch (NoSuchAlgorithmException e) {
            Log.e(TAG, "Implementation of SHA-256 algorithm not found. Authorisation failed. Exiting.");
            throw new RuntimeException();
        }
    }

    private void trySync() {
        if (Helper.getInstance(this).hasInternet()) {
            authorize();
            SyncHelper.getInstance(this).start();
            // if wither of these fail, they dump a stack trace to the log and execution continues
        }
        // if no internet, don't bother
    }

    private class GpsDataSender extends TimerTask {
        public void run() {
            if (gpsTracker.hasPosition()) {
                Log.d(TAG, "Sending new position over SAP");
                SAModel gpsData = new GpsPositionData(gpsTracker);
                // send to all connected peers
                for (RaceYourselfSamsungProviderConnection c : mConnectionsMap.values()) {
                    send(String.valueOf(c.mConnectionId), gpsData);
                }
            }
        }
    }

    /**
     * 
     * @param peerAgent
     * @param result
     */
    @Override
    protected void onFindPeerAgentResponse(SAPeerAgent peerAgent, int result) {

        Log.i(TAG, "onPeerAgentAvailable: Use this info when you want provider to initiate peer id = "
                + peerAgent.getPeerId());
        Log.i(TAG, "onPeerAgentAvailable: Use this info when you want provider to initiate peer name= "
                + peerAgent.getAccessory().getName());
    }

    /**
     * 
     * @param error
     * @param errorCode
     */
    @Override
    protected void onError(String error, int errorCode) {
        // TODO Auto-generated method stub
        Log.e(TAG, "ERROR: " + errorCode + ": " + error);
    }

    private void launchWebBrowser(String uri) {
        Log.i(TAG, "Launching browser for URI: " + uri);
        Intent myIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
        myIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        startActivity(myIntent);
    }

    /**
      * Service connection
      * Based on Samsung sample app by s.amit
      *
      */
    public class RaceYourselfSamsungProviderConnection extends SASocket {

        public static final String TAG = "RYSamsungProviderConnection";
        private int mConnectionId;

        /**
         * 
         */
        public RaceYourselfSamsungProviderConnection() {
            super(RaceYourselfSamsungProviderConnection.class.getName());
        }

        /**
         * 
         * @param channelId
         * @param data
         * @return
         */
        @Override
        public void onReceive(int channelId, byte[] data) {
            Log.i(TAG, "onReceive ENTER channel = " + channelId);
            String strToUpdateUI = new String(data);
            onDataAvailableOnChannel(String.valueOf(mConnectionId), channelId, //getRemotePeerId()
                    strToUpdateUI);
        }

        //@Override
        //      public void onSpaceAvailable(int channelId) {
        //          Log.v(TAG, "onSpaceAvailable: " + channelId);
        //      }

        /**
         * 
         * @param channelId
         * @param errorString
         * @param error
         */
        @Override
        public void onError(int channelId, String errorString, int error) {
            Log.e(TAG, "Connection is not alive ERROR: " + errorString + "  " + error);
        }

        /**
         * 
         * @param errorCode
         */
        @Override
        public void onServiceConnectionLost(int errorCode) {

            Log.e(TAG, "onServiceConectionLost  for peer = " + mConnectionId + "error code =" + errorCode);
            if (mConnectionsMap != null) {
                mConnectionsMap.remove(mConnectionId);
                if (mConnectionsMap.isEmpty()) {
                    // Disable 'tethered' icon
                    disableIcon();
                    // turn off GPS tracker and sensor service
                    Log.d(TAG, "No connections remaining, destroying GPS tracker");
                    stopTracking();
                }

            }

        }

    }

    private void enableIcon() {
        if (iconEnabled)
            return;
        iconEnabled = true;
        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this).setSmallIcon(R.drawable.rylogo)
                .setContentTitle(getString(R.string.notification_title))
                .setContentText(getString(R.string.notification_message));

        Intent intent = new Intent(this, PopupActivity.class);
        PendingIntent resultPendingIntent = PendingIntent.getActivity(this, 0, intent,
                PendingIntent.FLAG_UPDATE_CURRENT);
        mBuilder.setContentIntent(resultPendingIntent);

        // Gets an instance of the NotificationManager service
        NotificationManager mNotifyMgr = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        // Builds the notification and issues it.
        mNotifyMgr.notify(TETHER_NOTIFICATION_ID, mBuilder.build());
    }

    private void disableIcon() {
        if (!iconEnabled)
            return;
        // Gets an instance of the NotificationManager service
        NotificationManager mNotifyMgr = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        mNotifyMgr.cancel(TETHER_NOTIFICATION_ID);
        iconEnabled = false;
    }

    public static String MD5(String plaintext) {
        try {
            MessageDigest digest = MessageDigest.getInstance("MD5");
            digest.update(plaintext.getBytes());
            return String.format("%x", new BigInteger(digest.digest()));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }
}