com.digi.android.wva.VehicleInfoService.java Source code

Java tutorial

Introduction

Here is the source code for com.digi.android.wva.VehicleInfoService.java

Source

/* 
 * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of
 * the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
 * 
 * Copyright (c) 2014 Digi International Inc., All Rights Reserved. 
 */

package com.digi.android.wva;

import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;

import com.digi.android.wva.adapters.EndpointsAdapter;
import com.digi.android.wva.adapters.LogAdapter;
import com.digi.android.wva.model.EndpointConfiguration;
import com.digi.android.wva.model.LogEvent;
import com.digi.android.wva.util.MessageCourier;
import com.digi.android.wva.util.NetworkUtils;
import com.digi.android.wva.util.VehicleEndpointComparator;
import com.digi.wva.WVA;
import com.digi.wva.async.EventChannelStateListener;
import com.digi.wva.async.WvaCallback;

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

import java.io.IOException;
import java.util.List;
import java.util.Set;

/**
 * VehicleInfoService is a self-contained service created to facilitate easy
 *   integration with the Digi Wi-Fi Vehicle Bus Adapter (WVA). It is intended
 *   to be a started service which provides constant information from the WVA
 *   web service.
 *
 *   <p>In this demonstration app, this service is started when the application
 *   is created (in {@link WvaApplication#onCreate}) and by calling startService
 *   with intents containing various commands, the service can be directed to
 *   connect or disconnect from devices. The idea is that the service is always
 *   running, just not necessarily always doing useful things.</p>
 *
 * @author awickert
 *
 */
public class VehicleInfoService extends Service {
    /* Constants to be added as extras in startService calls for the service
     * to specify what "command" is being passed */
    // INTENT_* constants are the Intent extra names
    /** Intent extra key to indicate the "command" of the intent */
    public static final String INTENT_CMD = "command";
    /** Intent extra key to indicate the IP address to connect to, when the
     * command being used is {@link #CMD_CONNECT}.
     */
    public static final String INTENT_IP = "ip_addr";
    /** Intent extra key to give the basic-auth username to use with this device.
     */
    public static final String INTENT_AUTH_USER = "auth_user";
    /** Intent extra key to give the basic-auth password to use with this device.
     */
    public static final String INTENT_AUTH_PASS = "auth_pass";
    /** Intent extra key to indicate whether the HTTP connection should use HTTPS or not.
     */
    public static final String INTENT_HTTPS = "https";

    // CMD_* are values for INTENT_CMD extras
    /** This command is only used in {@link com.digi.android.wva.WvaApplication#onCreate()},
     * to initialize VehicleInfoService.
     */
    public static final int CMD_APPCREATE = 0; // WvaApplication.onCreate only
    /** Directs VehicleInfoService to attempt to connect to a device at the
     * IP address given by the Intent extra whose key is {@link #INTENT_CMD}.
     */
    public static final int CMD_CONNECT = 1; // start listening to device
    /** Directs VehicleInfoService to disconnect from the device. */
    public static final int CMD_DISCONNECT = 2; // stop listening to anything

    private static final String TAG = "VehicleInfoService";

    private static final int NOTIF_ID = 98866843; // WVANOTIF

    private WVA mDevice;

    private boolean isConnected = false;

    private String connectIp; // IP address to connect to

    private Handler mHandler;

    /**
     * Builds a new EventChannelStateListener specialized for use by the demo app.
     *
     * <p>This method is protected, rather than private, due to a bug between JaCoCo and
     * the Android build tools which causes the instrumented bytecode to be invalid when this
     * method is private:
     * <a href="http://stackoverflow.com/questions/17603192/dalvik-transformation-using-wrong-invoke-opcode" target="_blank">see StackOverflow question.</a>
     * </p>
     * @return a new event channel state listener to use
     */
    protected EventChannelStateListener makeStateListener() {
        return new EventChannelStateListener() {
            @Override
            public boolean runsOnUiThread() {
                return true;
            }

            private void log(final LogEvent event) {
                LogAdapter.getInstance().add(event);
            }

            @Override
            public void onConnected(WVA device) {
                Log.d(TAG, "connectionListener -- onConnected");
                MessageCourier.sendDashConnected(connectIp);

                log(new LogEvent("Connected to device.", null));

                // Ensure the service-running notification goes up.
                isConnected = true;

                showNotificationIfRunning();
            }

            @Override
            public void onError(WVA device, IOException error) {
                Log.e(TAG, "Device connection error", error);

                device.disconnectEventChannel(true);

                log(new LogEvent("An error occurred. Disconnecting...", null));

                String msg;
                if (error == null) {
                    msg = "Connection with the WVA device encountered some error.";
                } else if (!NetworkUtils.shouldBeAllowedToConnect(getApplicationContext())) {
                    Log.d(TAG, "Connection error is because the network went away.");
                    msg = "Your network connection has gone away.";
                } else {
                    msg = "Connection with the WVA device encountered an error: " + error.getMessage();
                }
                MessageCourier.sendError(msg);
            }

            @Override
            public void onRemoteClose(WVA device, int port) {
                Log.d(TAG, "connectionListener -- onRemoteClose");

                MessageCourier.sendReconnecting(connectIp);

                log(new LogEvent("Reconnecting...", null));

                // this will interrupt() the EventChannel thread, but since we're
                // inside that thread currently, and we're not doing any thread-blocking
                // calls here, execution will continue until we leave this method.
                // Then, execution returns to EventChannel, and off we go.
                super.onRemoteClose(device, port);
            }

            @Override
            public void onFailedConnection(WVA device, int port) {
                Log.d(TAG, "connectionListener -- onFailedConnection");
                MessageCourier.sendReconnecting(connectIp);
                log(new LogEvent("Retrying connection...", null));
                reconnectAfter(device, 15000, port);
            }
        };
    }

    public VehicleInfoService() {
        connectIp = null;
    }

    @Override
    public void onCreate() {
        Log.d(TAG, "Vehicle service created.");

        WvaApplication app = (WvaApplication) getApplication();
        if (app == null) {
            // This shouldn't happen in any reasonable scenario.
            throw new NullPointerException("Couldn't get application in service!");
        } else {
            mHandler = app.getHandler();
        }
        super.onCreate();
    }

    /**
     * Depending on if the service is currently set as 'connected'
     * (check value of {@code isConnected} boolean), either call
     * {@link #startForeground(int, Notification)} to put up the
     * "service is running" notification, or call
     * {@link #stopForeground(boolean) stopForeground(true)} to remove the
     * notification (because the service isn't listening).
      *
      * <p>This method is protected, rather than private, due to a bug between JaCoCo and
      * the Android build tools which causes the instrumented bytecode to be invalid when this
      * method is private:
      * http://stackoverflow.com/questions/17603192/dalvik-transformation-using-wrong-invoke-opcode
      * </p>
     */
    protected void showNotificationIfRunning() {
        // First, c
        WvaApplication app = (WvaApplication) getApplication();
        if (app != null && app.isTesting())
            return;

        if (isConnected) {
            NotificationCompat.Builder builder;
            builder = new NotificationCompat.Builder(getApplicationContext());
            PendingIntent contentIntent;
            Intent intent = new Intent(VehicleInfoService.this, DashboardActivity.class);
            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
            contentIntent = PendingIntent.getActivity(VehicleInfoService.this, 0, intent, 0);
            builder.setContentTitle("Digi WVA Service")
                    .setContentText("Connected to " + (TextUtils.isEmpty(connectIp) ? "(null)" : connectIp))
                    .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher))
                    .setSmallIcon(R.drawable.notif_small).setOngoing(true).setContentIntent(contentIntent);

            startForeground(NOTIF_ID, builder.build());
        } else {
            try {
                stopForeground(true);
            } catch (Exception e) {
                // Might happen if startForeground not called before
                e.printStackTrace();
            }
        }
    }

    /**
     * Factory function to create the intent used when startService is called
     * in {@link WvaApplication#onCreate onCreate}
     *
     * @param context Application context (use {@link #getApplicationContext()})
     * @return intent to be used in startService call
     */
    public static Intent buildCreateIntent(Context context) {
        // Make new intent for VehicleInfoService with command CMD_APPCREATE
        return new Intent(context, VehicleInfoService.class).putExtra(INTENT_CMD, CMD_APPCREATE);
    }

    /**
     * Factory function to create the intent used in a startService call to
     * tell the {@link VehicleInfoService} to "connect" to the device at
     * the given IP address.
     *
     * @param context Application context (use {@link #getApplicationContext()})
     * @param ip_addr IP address of device to connect to
     * @return intent to be used in startService call
     */
    public static Intent buildConnectIntent(Context context, String ip_addr) {
        return buildConnectIntent(context, ip_addr, null, null, true);
    }

    public static Intent buildConnectIntent(Context context, String ip_addr, String auth_user, String auth_pass,
            boolean useHttps) {
        // Make new intent with command CMD_CONNECT and the IP given
        Intent intent = new Intent(context, VehicleInfoService.class);
        // Add command and the ip address
        intent.putExtra(INTENT_CMD, CMD_CONNECT).putExtra(INTENT_IP, ip_addr).putExtra(INTENT_AUTH_USER, auth_user)
                .putExtra(INTENT_AUTH_PASS, auth_pass).putExtra(INTENT_HTTPS, useHttps);

        return intent;
    }

    /**
     * Factory function to create the intent used in a startService call to
     * tell the {@link VehicleInfoService} to "disconnect" from whatever
     * device it's connected to currently
     *
     * @param context Application context (use {@link #getApplicationContext()})
     * @return intent to be used in startService call
     */
    public static Intent buildDisconnectIntent(Context context) {
        return new Intent(context, VehicleInfoService.class).putExtra(INTENT_CMD, CMD_DISCONNECT);
    }

    /**
     * Take the Intent that was used to call startService (i.e. the
     * intent used to give a command to the service) and the command
     * that it had as an extra and act on it accordingly.
      *
      * <p>This method is protected, rather than private, due to a bug between JaCoCo and
      * the Android build tools which causes the instrumented bytecode to be invalid when this
      * method is private:
      * http://stackoverflow.com/questions/17603192/dalvik-transformation-using-wrong-invoke-opcode
      * </p>
     *
     * @param intent the intent used to call startService
     * @param command the command that the intent had as an extra
     */
    protected synchronized void parseIntent(Intent intent, int command) {
        boolean isConnect = false;
        final WvaApplication app = (WvaApplication) getApplication();
        if (app == null) {
            // Based on Android sources, this would only happen if the service is
            // not attached to an application... In this case, we can't know what
            // is a valid thing to do here.
            Log.e(TAG, "getApplication() returned null!");
            return;
        }
        switch (command) {
        case CMD_APPCREATE:
            Log.i(TAG, "startService - CMD_APPCREATE");
            isConnected = false;
            break;
        case CMD_CONNECT:
            Log.i(TAG, "startService - CMD_CONNECT");
            isConnect = true;
            break;
        case CMD_DISCONNECT:
            Log.i(TAG, "startService - CMD_DISCONNECT");
            isConnected = false;
            if (mDevice != null) {
                mDevice.disconnectEventChannel(true);
                mDevice = null;
                app.setDevice(null);
            } else {
                Log.d(TAG, "Got CMD_DISCONNECT but mDevice is null");
            }
            break;
        default:
            Log.i(TAG, "startService - unknown command " + command);
            isConnected = false;
        }

        if (isConnect) {
            String ip = intent.getStringExtra(INTENT_IP);
            String username = intent.getStringExtra(INTENT_AUTH_USER);
            String password = intent.getStringExtra(INTENT_AUTH_PASS);
            boolean useHttps = intent.getBooleanExtra(INTENT_HTTPS, true);
            if (TextUtils.isEmpty(ip)) {
                Log.e(TAG, "startService given connect command with empty IP!");
                isConnected = false;
            } else {
                final int port = Integer.valueOf(
                        PreferenceManager.getDefaultSharedPreferences(this).getString("pref_device_port", "5000"));
                connectIp = ip;
                isConnected = false;

                boolean autoSubscribe = PreferenceManager.getDefaultSharedPreferences(this)
                        .getBoolean("pref_auto_subscribe", false);
                final int autosub = autoSubscribe ? Integer.valueOf(PreferenceManager
                        .getDefaultSharedPreferences(this).getString("pref_default_interval", "-1")) : -1;

                Log.i(TAG, "Initiating connection to " + connectIp);

                // Structuring the code like this allows us to
                // inject Device instances for testing. Otherwise,
                // it would become difficult to unit-test
                // VehicleInfoService.

                mDevice = app.getDevice();
                if (mDevice == null) {
                    mDevice = new WVA(connectIp);

                    // Set up authentication and HTTP(S) configuration.
                    // The demo app assumes default HTTP and HTTPS ports.
                    mDevice.useBasicAuth(username, password).useSecureHttp(useHttps).setHttpPort(80)
                            .setHttpsPort(443);

                    app.setDevice(mDevice);
                }

                mDevice.fetchVehicleDataEndpoints(new WvaCallback<Set<String>>() {
                    @Override
                    public void onResponse(Throwable error, Set<String> endpoints) {
                        Log.d(TAG, "initVehicleData onResponse...");
                        if (error != null) {
                            Log.e(TAG, "Got error starting Vehicle", error);
                            // Stop WVA inner threads (TCPReceiver, MessageHandler)
                            synchronized (VehicleInfoService.this) {
                                mDevice.disconnectEventChannel();
                                mDevice = null;
                                app.setDevice(null);
                            }
                            String err = error.getMessage();
                            if (TextUtils.isEmpty(err)) {
                                Throwable cause = error.getCause();
                                if (cause != null)
                                    err = cause.getMessage();
                                else
                                    err = error.toString();
                            }
                            MessageCourier.sendError(err);
                            return;
                        }

                        // Sort the endpoints set
                        final List<String> sortedEndpoints = VehicleEndpointComparator.asSortedList(endpoints);

                        Log.d(TAG, "Beginning endpoint handling");

                        // First, add them all to the adapter.
                        for (String e : sortedEndpoints) {
                            // To improve performance, we add the endpoint to the endpoints list here,
                            // but do not notify it that the data set has changed. We then notify only
                            // after adding all endpoints.
                            EndpointsAdapter.getInstance().add(new EndpointConfiguration(e), false);
                        }

                        // Update the endpoints adapter.
                        mHandler.postAtFrontOfQueue(new Runnable() {
                            @Override
                            public void run() {
                                Log.d("VIS", "Updating endpoints adapter");
                                EndpointsAdapter.getInstance().notifyDataSetChanged();
                            }
                        });

                        // Handle subscribing/unsubscribing on a separate thread.
                        if (autosub > 0) {
                            Runnable doSubscriptions = new Runnable() {
                                @Override
                                public void run() {
                                    for (String e : sortedEndpoints) {
                                        // Add a bit of sleep between subscribing
                                        // to each endpoint, so as not to overload
                                        // the main thread as it tries to keep up.
                                        // This is happening as soon as the
                                        // DashboardActivity is launched, after all.
                                        try {
                                            Thread.sleep(25);
                                        } catch (InterruptedException ignored) {
                                        }

                                        if (app.getDevice() == null) {
                                            // User backed out of DashboardActivity
                                            // We should stop these subscriptions...
                                            Log.d(TAG, "app.getDevice() returned null. "
                                                    + "Stopping subscriptions...");
                                            app.clearDevice();
                                            return;
                                        }

                                        final String ep = e;

                                        boolean isPressurePro = false;
                                        for (String s : VehicleEndpointComparator.PRESSURE_PRO_PREFIXES) {
                                            if (ep.startsWith(s)) {
                                                isPressurePro = true;
                                                break;
                                            }
                                        }

                                        if (!isPressurePro) {
                                            // (Try to) subscribe to the endpoint
                                            app.subscribeToEndpointFromService(e, autosub, new WvaCallback<Void>() {
                                                @Override
                                                public void onResponse(Throwable error, Void response) {
                                                    String msg;
                                                    if (error != null) {
                                                        msg = "Failed to subscribe to " + ep;
                                                        Log.e(TAG, "Failed to subscribe to " + ep, error);
                                                        final LogEvent evt = new LogEvent(msg, null);
                                                        mHandler.post(new Runnable() {
                                                            @Override
                                                            public void run() {
                                                                LogAdapter.getInstance().add(evt);
                                                            }
                                                        });
                                                    }
                                                }
                                            });
                                        }
                                    }
                                }
                            };

                            // Do subscriptions on this thread when running unit tests,
                            // but on a separate thread when actually being used. This allows
                            // us to test the code in the runnable.
                            if (app.isTesting()) {
                                doSubscriptions.run();
                            } else {
                                new Thread(doSubscriptions).start();
                            }
                        }
                    }
                });

                if (mDevice == null)
                    return;

                // Ensure that the correct state listener is used.
                mDevice.setEventChannelStateListener(makeStateListener());
                mDevice.connectEventChannel(port);

                if (mDevice == null)
                    return;

                // Android Studio warns that getApplicationContext() might
                // return null. So we will check for null in the callbacks.
                final Context toastContext = getApplicationContext();

                JSONObject portJson = new JSONObject();
                try {
                    portJson.put("port", port);
                    portJson.put("enable", "on");
                } catch (JSONException e) {
                    e.printStackTrace();
                    return;
                }

                mDevice.configure("ws_events", portJson, new WvaCallback<Void>() {
                    @Override
                    public void onResponse(Throwable error, Void response) {
                        if (error == null) {
                            Log.d(TAG, "Successfully configured port.");
                        } else {
                            Log.d(TAG, "Failed to configure port", error);
                            if (toastContext != null) {
                                Toast.makeText(toastContext, "Failed to set port to " + port, Toast.LENGTH_SHORT)
                                        .show();
                            }
                        }
                    }
                });
            }
        }
    }

    @Override
    public int onStartCommand(Intent intent, int code, int startid) {

        //      Log.d(TAG, "VIS onStartCommand, got intent? " + (intent != null));
        if (intent == null) {
            // Process was killed and is being restarted, and we previously
            // returned START_STICKY, so this method is getting a null Intent.
            // Since the app was killed we don't know who to talk to, so
            // don't talk to anyone.
            Log.e(TAG, "onStartCommand - null intent");
            isConnected = false;
        } else {
            int command = intent.getIntExtra(INTENT_CMD, -1);
            if (command == -1) {
                Log.e(TAG, "startService called without command");
                isConnected = false;
            } else {
                parseIntent(intent, command);
            }
        }

        showNotificationIfRunning();

        return START_STICKY;
    }

    /**
     * Useful for unit testing, to be able to access the field and
     * find out what it is set to at any given instant.
     *
     * @return the service's current Device reference
     */
    public synchronized WVA getDevice() {
        return mDevice;
    }

    /**
     * Indicate if the VehicleInfoService is currently connected to a
     * WVA device
     * @return true if connected to a WVA device, false otherwise
     */
    public boolean isConnected() {
        return isConnected;
    }

    /**
     * Fetch the IP address we last attempted to connect to (whether that attempt
     * was successful or not)
     * @return the last IP address we tried to connect to
     */
    public String getConnectionIpAddress() {
        return connectIp;
    }

    @Override
    public void onDestroy() {
        //      Log.d(TAG, "onDestroy");

        // Remove the notification
        WvaApplication app = (WvaApplication) getApplication();
        if (app != null && !app.isTesting())
            stopForeground(true);
    }

    @Override
    public IBinder onBind(Intent intent) {
        // TODO Auto-generated method stub
        return null;
    }
}