/*
* 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.Application;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.Builder;
import android.text.TextUtils;
import android.util.Log;
import com.digi.addp.AddpClient;
import com.digi.android.wva.adapters.EndpointsAdapter;
import com.digi.android.wva.adapters.LogAdapter;
import com.digi.android.wva.adapters.VariableAdapter;
import com.digi.android.wva.model.EndpointConfiguration;
import com.digi.android.wva.model.EndpointConfiguration.AlarmConfig;
import com.digi.android.wva.model.EndpointConfiguration.SubscriptionConfig;
import com.digi.android.wva.model.VehicleData;
import com.digi.android.wva.util.MessageCourier;
import com.digi.android.wva.util.VehicleDataList;
import com.digi.wva.WVA;
import com.digi.wva.async.AlarmType;
import com.digi.wva.async.EventFactory;
import com.digi.wva.async.VehicleDataEvent;
import com.digi.wva.async.VehicleDataListener;
import com.digi.wva.async.VehicleDataResponse;
import com.digi.wva.async.WvaCallback;
import org.joda.time.DateTime;
import java.util.Arrays;
import java.util.List;
//import com.digi.connector.android.library.core.CloudConnectorManager;
//import com.digi.connector.android.library.models.Sample;
/**
* Custom {@link Application} object to provide global variables and
* context across the app.
*
* <p>First and foremost, {@link WvaApplication}
* creates the singleton {@link LogAdapter}, {@link VehicleDataList},
* {@link VariableAdapter}, and {@link EndpointsAdapter}
* objects used across the application, and also starts up the
* {@link VehicleInfoService} service.</p>
*
* @author mwadsten
*
*/
public class WvaApplication extends Application {
/** Notification ID used by alarm notifications.
* <p><b>0x98225216</b> is how you would type "WVAALARM" on
* a keypad.</p> */
private static final int ALARM_NOTIF_ID = 0x98225216; // WVAALARM
private static final String TAG = "WvaApplication";
private static String appVersion;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private WVA mDevice;
private AddpClient addpClient;
//private CloudConnectorManager mCloudConnectorManager;
public Handler getHandler() {
// This method seems to not work when running unit tests.
if (isTesting())
return new Handler(Looper.getMainLooper());
return mHandler;
}
/**
* Set the ADDP client to be used in the application for device
* discovery.
*
* <p>(Useful in testing, as one can use this method to set the
* ADDP client to be a mock implementation, and use that for testing.)</p>
* @param client {@link AddpClient} to use
*/
public void setAddpClient(AddpClient client) {
this.addpClient = client;
}
/**
* Fetch the ADDP client to be used for discovery
* @return the current ADDP client to use, or null if it has not been set
*/
public AddpClient getAddpClient() {
return addpClient;
}
/*
public CloudConnectorManager getCloudConnector() {
return mCloudConnectorManager;
}
*/
public String getApplicationVersion() {
return appVersion;
}
// Every subscription listener will have the exact same behavior, so there's
// no real reason to create new VehicleDataListener instances per subscription when
// we can route all data through a single "static" listener.
// (I say "static" because this listener is not static as far as the
// keyword goes, but it is declared final...)
//-----
// Another benefit of routing all listener callbacks through here is that
// we can easily tie receipt of subscription data to arbitrary actions,
// like notifying the data chart activity of new data.
private final VehicleDataListener dataListener = new VehicleDataListener() {
// TODO get definite endpoints names for these
private final List<String> graphingEndpoints = Arrays.asList("VehicleSpeed", "EngineSpeed");
@Override
public boolean runsOnUiThread() {
// This override is not strictly necessary, but it's good to be explicit.
return true;
}
@Override
public void onEvent(VehicleDataEvent event) {
// "endpoint" is made final so it can be used in the new Runnable below
final String endpoint = event.getEndpoint();
Log.d("WVAApplication", "Listener cb for endpoint " + endpoint);
VehicleDataResponse response = event.getResponse();
Double value = response.getValue();
DateTime time = response.getTime();
final VehicleData newData =
new VehicleData(endpoint, value, time);
if (event.getType() == EventFactory.Type.SUBSCRIPTION) {
Log.v(TAG, "New data: " + endpoint + "=" + value
+ " @ " + time.toString());
// Add the new data to the variable adapter.
VariableAdapter.getInstance().add(newData);
// If this newly received data point is one of the "graphable"
// endpoints (i.e. one of those displayed when the user chooses
// to see the graph), send the data out on the local broadcast
// so the graph activity can pick it up, if it is live.
if (graphingEndpoints.contains(endpoint)) {
MessageCourier.sendChartNewData(newData);
}
// Fetch EndpointConfiguration for this endpoint, check if it should be pushed
// to Device Cloud, and send the sample if need be.
EndpointConfiguration cfg = EndpointsAdapter.getInstance().findEndpointConfiguration(endpoint);
// Skip sending to Device Cloud if no EndpointConfiguration exists, or if the user has not subscribed
// to this endpoint.
if (cfg == null || !cfg.isSubscribed()) {
return;
}
/**
else if (cfg.shouldBePushedToDeviceCloud()) {
Sample s = new Sample("wva", endpoint, String.valueOf(value));
mCloudConnectorManager.sendSample("upload.xml", s);
}**/
} else if (event.getType() == EventFactory.Type.ALARM) {
Log.v("WVAApplication", "Alarm triggered by " + endpoint);
// Log an event saying the alarm went off
LogAdapter.getInstance().alarmTriggered(newData);
showAlarmNotification(endpoint, newData);
}
}
};
//==========================================================================
// Methods not directly related to WVALib interactivity.
@Override
public void onCreate() {
super.onCreate();
// Get app version from package manager, hold onto it locally
final String UNAVAILABLE = "N/A";
String pkgName = getPackageName();
String versionName = UNAVAILABLE;
try {
PackageManager pm = getPackageManager();
if (pm == null)
Log.e(TAG, "Can't get app version; package manager is null");
else {
PackageInfo pi = pm.getPackageInfo(pkgName, 0);
if (pi != null)
versionName = pi.versionName;
else
Log.e(TAG, "Couldn't get app version: no package info");
}
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Couldn't get app version: NameNotFoundException");
}
appVersion = versionName;
// Initialize global singleton objects.
createSingletons();
// "Start" the VehicleInfoService
startService(VehicleInfoService.buildCreateIntent(this));
//mCloudConnectorManager = new CloudConnectorManager(this);
}
void createSingletons() {
LogAdapter.initInstance(this);
VehicleDataList.initInstance();
VariableAdapter.initInstance(this,
VehicleDataList.getInstance());
EndpointsAdapter.initInstance(this);
}
//==========================================================================
// Methods related to WVALib interactivity (manipulating the WVA object,
// handling alarms, subscribing to endpoints, etc.)
/**
* Fetch the {@link WVA} object currently used for connection to a
* WVA device
* @return the currently-used WVA, or null if no device connection is
* active
*/
public WVA getDevice() {
return mDevice;
}
/**
* Set a reference to the currently active {@link WVA} object
* @param dev the WVA object in use
*/
public void setDevice(WVA dev) {
mDevice = dev;
}
/**
* (For testing.) Fetch the {@link VehicleDataListener} used as a listener for
* new subscription data by the application.
* @return the subscriptions listener for the app
*/
public VehicleDataListener getDataListener() {
return dataListener;
}
/**
* Ensures that the {@link WVA} object reference held by the app is nullified.
*/
public void clearDevice() {
if (mDevice == null) return;
// mDevice.vehicle.removeAllCallbacks();
mDevice = null; // drop reference to the vehicle
}
/**
* Subscribe (asynchronously) to the given endpoint with the given subscription interval.
* Same as {@link #subscribeToEndpoint(String, int, WvaCallback)}, except that it doesn't
* notify the EndpointsAdapter, to improve performance on initial load.
*
* @see {@link #subscribeToEndpoint(String, int, WvaCallback)}.
*/
public void subscribeToEndpointFromService(String endpoint, int interval, WvaCallback<Void> callback) {
this.subscribeToEndpoint(endpoint, interval, callback, false);
}
/**
* Subscribe (asynchronously) to the given endpoint with the given
* subscription interval.
*
* <p><b>Note:</b> This method has no way of giving direct feedback to
* the caller that the subscription call succeeded or failed.</p>
* @param endpoint name of the endpoint to subscribe to
* @param interval time interval to receive subscription data
* @param callback {@link WvaCallback} to be invoked when the subscription
* web-services call goes through (or fails)
*/
public void subscribeToEndpoint(final String endpoint,
final int interval,
final WvaCallback<Void> callback) {
this.subscribeToEndpoint(endpoint, interval, callback, true);
}
/**
* Implementation behind {@link #subscribeToEndpoint(String, int, WvaCallback)} and
* {@link #subscribeToEndpointFromService(String, int, WvaCallback)}.
*
* <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 subscribeToEndpoint(final String endpoint,
final int interval,
final WvaCallback<Void> callback,
final boolean notify) {
if (mDevice == null) {
Log.e(TAG, "addSubscriptionToEndpoint - mDevice is null");
callback.onResponse(new NullPointerException("No device."), null);
return;
}
// Ensure that the correct vehicle data listener is being used.
mDevice.setVehicleDataListener(dataListener);
mDevice.subscribeToVehicleData(endpoint, interval, callback);
boolean needsToBeAdded = false;
SubscriptionConfig subconf = new SubscriptionConfig(interval);
subconf.setSubscribed(true);
EndpointConfiguration ept = EndpointsAdapter.getInstance().findEndpointConfiguration(endpoint);
if (ept == null) {
needsToBeAdded = true;
ept = new EndpointConfiguration(endpoint);
}
ept.setSubscriptionConfig(subconf);
final EndpointConfiguration ec = ept;
final boolean needToAdd = needsToBeAdded;
mHandler.post(new Runnable() {
@Override
public void run() {
EndpointsAdapter epts = EndpointsAdapter.getInstance();
if (needToAdd) {
// This will call notifyDataSetChanged once the new
// configuration is added to the adapter.
epts.add(ec, notify);
} else if (notify) {
// We modified the subscription setup, so we need to
// call notifyDataSetChanged to ensure the new information
// is reflected in the list view.
epts.notifyDataSetChanged();
}
}
});
}
/**
* Add a new, empty {@link EndpointConfiguration} to the
* {@link EndpointsAdapter}.
* @param endpoint name of the endpoint to add to the list
*/
public void listNewEndpoint(String endpoint) {
listNewEndpoint(endpoint, false);
}
/**
* Add a new, empty {@link EndpointConfiguration} to the
* {@link EndpointsAdapter}.
* @param endpoint name of the endpoint to add to the list
* @param notify set <b>true</b> to notify the adapter that the data set has changed.
* This is set to <b>false</b> in {@link VehicleInfoService} to improve performance on initial
* endpoint list loading.
*/
public void listNewEndpoint(String endpoint, final boolean notify) {
final EndpointConfiguration conf = new EndpointConfiguration(endpoint);
mHandler.post(new Runnable() {
@Override
public void run() {
EndpointsAdapter.getInstance().add(conf, notify);
}
});
}
/**
* Wrapper around calling EndpointsAdapter.notifyDataSetChanged
* on the main thread.
*/
void notifyEndpointsChanged() {
mHandler.post(new Runnable() {
@Override
public void run() {
EndpointsAdapter.getInstance().notifyDataSetChanged();
}
});
}
/**
* Unsubscribe (asynchronously) from the given endpoint.
*
* <p><b>Note:</b> This method has no way of giving direct feedback to
* the caller that the subscription call succeeded or failed.</p>
* @param endpoint endpoint to unsubscribe from
* @param callback {@link WvaCallback} to be invoked when the unsubscribe
* web-services call goes through (or fails)
*/
public void unsubscribe(final String endpoint, final WvaCallback<Void> callback) {
if (mDevice == null) {
Log.e(TAG, "unsubscribe called when device was null");
callback.onResponse(new NullPointerException("No device"), null);
return;
}
mDevice.unsubscribeFromVehicleData(endpoint, callback);
final EndpointConfiguration conf =
EndpointsAdapter.getInstance().findEndpointConfiguration(endpoint);
if (conf != null) {
conf.setSubscriptionConfig(null);
notifyEndpointsChanged();
}
// Refresh vehicle data list
mHandler.post(new Runnable() {
@Override
public void run() {
VariableAdapter.getInstance().notifyDataSetChanged();
}
});
}
/**
* Create a new alarm for data from the given endpoint, with the alarm
* specified by the other parameters.
*
* @param endpoint data endpoint to set up an alarm for
* @param type type of alarm (above, below, etc. See {@link AlarmType})
* @param interval alarm interval
* @param threshold threshold for alarm
* @param callback {@link WvaCallback} to be invoked when the alarm creation
* web-services call goes through (or fails)
*/
public void createAlarm(final String endpoint, AlarmType type,
double threshold, int interval,
final WvaCallback<Void> callback) {
if (mDevice == null) {
Log.e(TAG, "Could not create alarm; no associated device!");
callback.onResponse(new NullPointerException("No device"), null);
return;
}
// Log.i("WVAApplication", "Creating alarm on " + endpoint);
// Ensure that the correct vehicle data listener is being used.
mDevice.setVehicleDataListener(dataListener);
mDevice.createVehicleDataAlarm(endpoint, type, (float) threshold, interval, callback);
boolean needToAdd = false;
EndpointConfiguration c = EndpointsAdapter.getInstance().findEndpointConfiguration(endpoint);
if (c == null) {
Log.d(TAG, "Creating new endpoint configuration");
c = new EndpointConfiguration(endpoint);
needToAdd = true;
}
AlarmConfig ac = new AlarmConfig(type, threshold, interval);
ac.setCreated(true);
c.setAlarmConfig(ac);
final EndpointConfiguration conf = c;
final boolean needToAddFinal = needToAdd;
// Only if vehicle.createAlarm worked
mHandler.post(new Runnable() {
@Override
public void run() {
EndpointsAdapter adapter = EndpointsAdapter.getInstance();
if (!needToAddFinal) {
// We still need to call notifyDataSetChanged
adapter.notifyDataSetChanged();
}
else {
// This calls notifyDataSetChanged
adapter.add(conf);
}
}
});
}
/**
* Delete any alarms matching the arguments (e.g. "EngineSpeed" and
* ABOVE) from the WVA.
* @param endpoint endpoint name whose data is evaluated for alarms
* @param type type of alarm that needs to be deleted
* @param callback {@link WvaCallback} to be invoked when the alarm removal
* web-services call goes through (or fails)
*/
public void removeAlarm(String endpoint, AlarmType type, final WvaCallback<Void> callback) {
Log.d(TAG, "removeAlarm " + endpoint + AlarmType.makeString(type));
if (mDevice == null) {
Log.e(TAG, "Cannot remove alarm; no associated device!");
callback.onResponse(new NullPointerException("No device"), null);
return;
}
mDevice.deleteVehicleDataAlarm(endpoint, type, callback);
final EndpointConfiguration conf =
EndpointsAdapter.getInstance().findEndpointConfiguration(endpoint);
if (conf != null) {
// We have an alarm configuration to "forget about"
conf.setAlarmConfig(null);
mHandler.post(new Runnable() {
@Override
public void run() {
EndpointsAdapter.getInstance().notifyDataSetChanged();
}
});
}
}
/**
* Display a status bar notification about the alarm.
*
* @param alarmName name of alarm
* @param data VehicleData with data that triggered the alarm
*/
void showAlarmNotification(String alarmName, VehicleData data) {
NotificationManager nm = (NotificationManager)
getSystemService(Context.NOTIFICATION_SERVICE);
NotificationCompat.Builder builder = new Builder(this);
Intent activityIntent = new Intent(this, DashboardActivity.class);
activityIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_SINGLE_TOP);
// Get alarm sound out of preferences
SharedPreferences sp = PreferenceManager
.getDefaultSharedPreferences(this);
String ringtoneStr = sp.getString("pref_key_alarm_tone", null);
// Make items passed in to builder methods
Bitmap largeIcon = BitmapFactory.decodeResource(
getResources(), R.drawable.ic_launcher);
Uri ringtone = TextUtils.isEmpty(ringtoneStr) ? null : Uri.parse(ringtoneStr);
PendingIntent pend = PendingIntent.getActivity(
this, 0, activityIntent, 0);
// Build the notification and show it
builder.setLargeIcon(largeIcon)
.setSmallIcon(R.drawable.notif_small)
.setContentTitle("WVA Alarm: " + alarmName)
.setContentText("Value: " + data.value)
.setWhen(System.currentTimeMillis())
.setAutoCancel(true)
.setSound(ringtone)
.setContentIntent(pend)
.setTicker("WVA Alarm: " + alarmName);
nm.notify(ALARM_NOTIF_ID, builder.build());
}
/**
* Dismiss the alarm notification from the status bar, if it's there.
* Essentially the opposite of calling
* {@link #showAlarmNotification(String, VehicleData)}
*/
public void dismissAlarmNotification() {
NotificationManager nm = (NotificationManager)
getSystemService(Context.NOTIFICATION_SERVICE);
nm.cancel(ALARM_NOTIF_ID);
}
/**
* Indicated whether the WvaApplication is being used as part of
* unit testing, or not. The main usage of this information is
* to prevent things like notifications from popping up (as they
* are not going to be tested and only complicate things. Also,
* calling start/stopForeground from VehicleInfoService under
* unit testing causes NullPointerExceptions inside Android.
*
* <p>This method is meant to be mocked to return true when testing.</p>
* @return true if the application is being unit-tested as opposed to
* running in a production environment
*/
public boolean isTesting() {
return false;
}
}