package com.openxc.remote;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import com.openxc.DataPipeline;
import com.openxc.interfaces.VehicleInterface;
import com.openxc.interfaces.VehicleInterfaceDescriptor;
import com.openxc.interfaces.VehicleInterfaceException;
import com.openxc.interfaces.VehicleInterfaceFactory;
import com.openxc.interfaces.bluetooth.BluetoothVehicleInterface;
import com.openxc.messages.MessageKey;
import com.openxc.messages.VehicleMessage;
import com.openxc.sinks.DataSinkException;
import com.openxc.sinks.RemoteCallbackSink;
import com.openxc.sources.ApplicationSource;
import com.openxc.sources.DataSourceException;
import com.openxc.sources.NativeLocationSource;
import com.openxc.sources.VehicleDataSource;
import com.openxc.sources.WakeLockManager;
import com.openxcplatform.R;
/**
* The VehicleService is the centralized source of all vehicle data.
*
* This server is intended to be a singleton on an Android device. All OpenXC
* applciations funnel data to and from this service so they can share sources,
* sinks and vehicle interfaces.
*
* Applications should not use this service directly, but should bind to the
* in-process {@link com.openxc.VehicleManager} instead - that has an interface
* that respects Measurement types. The interface used for the
* VehicleService is purposefully primitive as there are a small set of
* objects that can be natively marshalled through an AIDL interface.
*
* Only one vehicle interface can be active at at time, and it can be set with
* the {@link #setVehicleInterface(String, String)} method.
*
* This service uses the same {@link com.openxc.DataPipeline} as the
* {@link com.openxc.VehicleManager} to move data from sources to sinks, but it
* the pipeline is not modifiable by the application as there is no good way to
* pass running sources through the AIDL interface. The same style is used here
* for clarity and in order to share code.
*/
public class VehicleService extends Service implements DataPipeline.Operator {
private final static String TAG = "VehicleService";
private final static int SERVICE_NOTIFICATION_ID = 1000;
// Work around an issue with instrumentation tests and foreground services
// https://code.google.com/p/android/issues/detail?id=12122
public static boolean sIsUnderTest = false;
private boolean mForeground = false;
private DataPipeline mPipeline = new DataPipeline(this);
private ApplicationSource mApplicationSource = new ApplicationSource();
private VehicleDataSource mNativeLocationSource;
private VehicleInterface mVehicleInterface;
private RemoteCallbackSink mNotifier = new RemoteCallbackSink();
private WakeLockManager mWakeLocker;
private boolean mUserPipelineActive;
private final RemoteCallbackList<ViConnectionListener> mViConnectionListeners =
new RemoteCallbackList<>();
@Override
public void onCreate() {
super.onCreate();
Log.i(TAG, "Service starting");
mWakeLocker = new WakeLockManager(this, TAG);
}
/**
* Shut down any associated services when this service is about to die.
*
* This stops the data source (e.g. stops trace playback) and kills the
* thread used for notifying measurement listeners.
*/
@Override
public void onDestroy() {
Log.i(TAG, "Service being destroyed");
mPipeline.stop();
}
/**
* Initialize the service and data source when a client binds to us.
*/
@Override
public IBinder onBind(Intent intent) {
Log.i(TAG, "Service binding in response to " + intent);
initializeDefaultSources();
initializeDefaultSinks(mPipeline);
return mBinder;
}
private void moveToForeground() {
if(!mForeground) {
Log.i(TAG, "Moving service to foreground.");
try {
// I'd like to not have to depend on the EnablerActivity, but
// the notification needs to have some application it starts
// when the users clicks the notification.
Intent intent = new Intent(this,
Class.forName("com.openxc.enabler.OpenXcEnablerActivity"));
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |
Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(
this, 0, intent, 0);
NotificationCompat.Builder notificationBuilder =
new NotificationCompat.Builder(this);
notificationBuilder.setContentTitle(getString(R.string.openxc_name))
.setContentInfo(getString(R.string.notification_content))
.setSmallIcon(R.drawable.openxc_notification_icon_small_white)
.setContentIntent(pendingIntent);
startForeground(SERVICE_NOTIFICATION_ID,
notificationBuilder.build());
} catch (ClassNotFoundException e) {
// This may happen if you are running the instrumentation tests
// and it's not an error
}
mForeground = true;
}
}
private void removeFromForeground() {
if(mForeground) {
Log.i(TAG, "Removing service from foreground.");
if(!sIsUnderTest) {
stopForeground(true);
}
mForeground = false;
}
}
private void initializeDefaultSinks(DataPipeline pipeline) {
pipeline.addSink(mNotifier);
}
private void initializeDefaultSources() {
mPipeline.addSource(mApplicationSource);
}
private final VehicleServiceInterface.Stub mBinder =
new VehicleServiceInterface.Stub() {
@Override
public VehicleMessage get(MessageKey key) {
return mPipeline.get(key);
}
@Override
public boolean send(VehicleMessage command) {
command.untimestamp();
boolean sent = false;
synchronized(VehicleService.this) {
if(mVehicleInterface != null && mVehicleInterface.isConnected()) {
try {
mVehicleInterface.receive(command);
Log.d(TAG, "Sent " + command + " using interface " +
mVehicleInterface);
sent = true;
} catch(DataSinkException e) {
Log.w(TAG, mVehicleInterface +
" unable to send command", e);
}
} else {
Log.w(TAG, "No connected VI available to send command");
}
}
return sent;
}
@Override
public void receive(VehicleMessage measurement) {
mApplicationSource.handleMessage(measurement);
}
@Override
public void register(VehicleServiceListener listener) {
Log.i(TAG, "Adding listener " + listener);
mNotifier.register(listener);
}
@Override
public void unregister(VehicleServiceListener listener) {
Log.i(TAG, "Removing listener " + listener);
mNotifier.unregister(listener);
}
@Override
public int getMessageCount() {
return VehicleService.this.mPipeline.getMessageCount();
}
@Override
public void setVehicleInterface(String interfaceName,
String resource) {
VehicleService.this.setVehicleInterface(
interfaceName, resource);
}
@Override
public void addViConnectionListener(ViConnectionListener listener) {
VehicleService.this.addViConnectionListener(listener);
}
@Override
public void setBluetoothPollingStatus(boolean enabled) {
VehicleService.this.setBluetoothPollingStatus(enabled);
}
@Override
public void setNativeGpsStatus(boolean enabled) {
VehicleService.this.setNativeGpsStatus(enabled);
}
@Override
public VehicleInterfaceDescriptor getVehicleInterfaceDescriptor() {
VehicleInterfaceDescriptor descriptor = null;
synchronized(VehicleService.this) {
if(mVehicleInterface != null) {
descriptor = new VehicleInterfaceDescriptor(mVehicleInterface);
}
}
return descriptor;
}
@Override
public void userPipelineActivated() {
mUserPipelineActive = true;
VehicleService.this.onPipelineActivated();
}
@Override
public void userPipelineDeactivated() {
mUserPipelineActive = false;
if(!VehicleService.this.mPipeline.isActive()) {
VehicleService.this.onPipelineDeactivated();
}
}
@Override
public boolean isViConnected() {
return VehicleService.this.mPipeline.isActive();
}
};
private void addViConnectionListener(ViConnectionListener listener) {
synchronized(mViConnectionListeners) {
mViConnectionListeners.register(listener);
}
}
private void setVehicleInterface(String interfaceName, String resource) {
Class<? extends VehicleInterface> interfaceType = null;
if(interfaceName != null) {
try {
interfaceType = VehicleInterfaceFactory.findClass(interfaceName);
} catch(VehicleInterfaceException e) {
Log.w(TAG, "Unable to find VI matching " + interfaceName +
" -- disabling current interface");
}
}
synchronized(this) {
if(mVehicleInterface != null && (interfaceName == null ||
(interfaceType != null &&
!mVehicleInterface.getClass().isAssignableFrom(
interfaceType)))) {
Log.i(TAG, "Disabling currently active VI " + mVehicleInterface);
mVehicleInterface.stop();
mPipeline.removeSource(mVehicleInterface);
mVehicleInterface = null;
}
if(interfaceName != null && interfaceType != null) {
if(mVehicleInterface == null ||
!mVehicleInterface.getClass().isAssignableFrom(
interfaceType)) {
try {
mVehicleInterface = VehicleInterfaceFactory.build(
interfaceType, VehicleService.this, resource);
} catch(VehicleInterfaceException e) {
Log.w(TAG, "Unable to set vehicle interface", e);
return;
}
mPipeline.addSource(mVehicleInterface);
} else {
try {
if(mVehicleInterface.setResource(resource)) {
Log.d(TAG, "Changed resource of already " +
"active interface " + mVehicleInterface);
} else {
Log.d(TAG, "Interface " + mVehicleInterface +
" already had same active resource " + resource +
" -- not restarting");
}
} catch(DataSourceException e) {
Log.w(TAG, "Unable to change resource", e);
}
}
}
Log.i(TAG, "Set vehicle interface to " + mVehicleInterface);
}
}
private void setNativeGpsStatus(boolean enabled) {
Log.i(TAG, "Setting native GPS to " + enabled);
if(enabled && mNativeLocationSource == null) {
mNativeLocationSource = new NativeLocationSource(this);
mPipeline.addSource(mNativeLocationSource);
} else if(!enabled) {
mPipeline.removeSource(mNativeLocationSource);
mNativeLocationSource = null;
}
}
private synchronized void setBluetoothPollingStatus(boolean enabled) {
if(mVehicleInterface != null &&
mVehicleInterface instanceof BluetoothVehicleInterface) {
((BluetoothVehicleInterface)mVehicleInterface).setPollingStatus(
enabled);
}
}
@Override
public synchronized void onPipelineActivated() {
mWakeLocker.acquireWakeLock();
moveToForeground();
if(mVehicleInterface != null && mVehicleInterface.isConnected()) {
VehicleInterfaceDescriptor descriptor =
new VehicleInterfaceDescriptor(mVehicleInterface);
synchronized(mViConnectionListeners) {
int i = mViConnectionListeners.beginBroadcast();
while(i > 0) {
i--;
try {
mViConnectionListeners.getBroadcastItem(i).onConnected(descriptor);
} catch(RemoteException e) {
Log.w(TAG, "Couldn't notify VI connection " +
"listener -- did it crash?", e);
}
}
mViConnectionListeners.finishBroadcast();
}
}
}
@Override
public void onPipelineDeactivated() {
if(!mUserPipelineActive) {
mWakeLocker.releaseWakeLock();
removeFromForeground();
synchronized(this) {
if(mVehicleInterface == null || !mVehicleInterface.isConnected()) {
synchronized(mViConnectionListeners) {
int i = mViConnectionListeners.beginBroadcast();
while(i > 0) {
i--;
try {
mViConnectionListeners.getBroadcastItem(i).onDisconnected();
} catch(RemoteException e) {
Log.w(TAG, "Couldn't notify VI connection " +
"listener -- did it crash?", e);
}
}
mViConnectionListeners.finishBroadcast();
}
}
}
}
}
}