/* Copyright (c) 2013 The MITRE Corporation, All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package org.mitre.svmp.services; import android.app.*; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.os.*; import android.util.Log; import android.widget.Toast; import org.mitre.svmp.activities.ConnectionList; import org.mitre.svmp.apprtc.AppRTCClient; import org.mitre.svmp.apprtc.MessageHandler; import org.mitre.svmp.client.*; import org.mitre.svmp.common.*; import org.mitre.svmp.common.StateMachine.STATE; import org.mitre.svmp.performance.PerformanceAdapter; import org.mitre.svmp.protocol.SVMPProtocol; import org.mitre.svmp.protocol.SVMPProtocol.AuthResponse.AuthResponseType; import org.mitre.svmp.protocol.SVMPProtocol.Response; /** * @author Joe Portner * An activity should use this service as follows: * 1. If the state is not NEW and the connectionID is different, stop the service * 2. Start the service (so it doesn't stop on unbind) * 3. Bind to the service */ public class SessionService extends Service implements StateObserver, MessageHandler, SensorEventListener, Constants { private static final String TAG = SessionService.class.getName(); private static final int NOTIFICATION_ID = 0; // only one service is started at a time, acts as a singleton for static getters private static SessionService service; // public getters for state and connectionID (used by activities) public static STATE getState() { STATE value = STATE.NEW; if (service != null && service.machine != null) value = service.machine.getState(); return value; } public static int getConnectionID() { int value = 0; if (service != null && service.connectionInfo != null) value = service.connectionInfo.getConnectionID(); return value; } public static boolean isRunningForConn(int connectionID) { return getConnectionID() == connectionID && getState() != StateMachine.STATE.NEW; } // local variables private AppRTCClient binder; // Binder given to clients private StateMachine machine; private PerformanceAdapter performanceAdapter; private NotificationManager notificationManager; private Handler handler; private DatabaseHandler databaseHandler; private ConnectionInfo connectionInfo; private boolean keepNotification; // client components private LocationHandler locationHandler; private SensorHandler sensorHandler; @Override public void onCreate() { Log.v(TAG, "onCreate"); service = this; machine = new StateMachine(); performanceAdapter = new PerformanceAdapter(); notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); handler = new Handler(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.v(TAG, "onStartCommand"); if (ACTION_STOP_SERVICE.equals(intent.getAction())) { stopSelf(); } else if (getState() == STATE.NEW) { // change state and get connectionID from intent machine.setState(STATE.STARTED, 0); int connectionID = intent.getIntExtra("connectionID", 0); // begin connecting to the server startup(connectionID); } return START_NOT_STICKY; // run until explicitly stopped. } @Override public IBinder onBind(Intent intent) { Log.v(TAG, String.format("onBind (state: %s)", getState())); return binder; } @Override public void onDestroy() { Log.v(TAG, "onDestroy"); // before we destroy this service, shut down its components shutdown(); super.onDestroy(); } private void startup(int connectionID) { Log.i(TAG, "Starting background service."); // connect to the database databaseHandler = new DatabaseHandler(this); // get connection information from database connectionInfo = databaseHandler.getConnectionInfo(connectionID); // create binder object binder = new AppRTCClient(this, machine, connectionInfo); // attach the performance adapter to the binder's performance data objects performanceAdapter.setPerformanceData(binder.getPerformance()); // create a location handler object locationHandler = new LocationHandler(this); // create a sensor handler object sensorHandler = new SensorHandler(this, performanceAdapter); // show notification showNotification(true); } private void shutdown() { Log.i(TAG, "Shutting down background service."); // reset singleton service = null; // send intent to ConnectionList to notify it to check for active services again Intent intent = new Intent(ACTION_REFRESH); sendBroadcast(intent, PERMISSION_REFRESH); // hide notification hideNotification(); // clean up location updates if (locationHandler != null) locationHandler.cleanupLocationUpdates(); // clean up sensor updates if (sensorHandler != null) sensorHandler.cleanupSensors(); // disconnect from the database if (databaseHandler != null) databaseHandler.close(); // try to disconnect the client object performanceAdapter.clearPerformanceData(); if (binder != null) { binder.disconnect(); binder = null; } } private void showNotification(boolean connected) { Notification.Builder notice = new Notification.Builder(this); Resources resources = getResources(); CharSequence contentTitle = resources.getText(R.string.sessionService_notification_contentTitle); String contentText; if (connected) { contentText = (String)resources.getText(R.string.sessionService_notification_contentText_connected); notice.setSmallIcon(R.drawable.svmp_status_green); } else { // we need authentication, indicate that in the notification contentText = (String)resources.getText(R.string.sessionService_notification_contentText_disconnected); notice.setSmallIcon(R.drawable.svmp_status_yellow); } notice.setContentTitle(contentTitle) .setContentText(String.format(contentText, connectionInfo.getDescription())) .setOngoing(true); // Creates an explicit intent for the ConnectionList Intent resultIntent = new Intent(this, ConnectionList.class); resultIntent.putExtra("connectionID", connectionInfo.getConnectionID()); PendingIntent resultPendingIntent; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { // The stack builder object will contain an artificial back stack for the // started Activity. // This ensures that navigating backward from the Activity leads out of // your application to the Home screen. TaskStackBuilder stackBuilder = TaskStackBuilder.create(this); // Adds the back stack for the Intent (but not the Intent itself) stackBuilder.addParentStack(ConnectionList.class); // Adds the Intent that starts the Activity to the top of the stack stackBuilder.addNextIntent(resultIntent); resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); } else { resultPendingIntent = PendingIntent.getActivity(this, 0, resultIntent, PendingIntent.FLAG_UPDATE_CURRENT); } notice.setContentIntent(resultPendingIntent); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { // add "Open" action CharSequence openText = resources.getText(R.string.sessionService_notification_action_open); notice.addAction(android.R.drawable.ic_media_play, openText, resultPendingIntent); // add "Exit" action CharSequence exitText = resources.getText(R.string.sessionService_notification_action_exit); Intent stopServiceIntent = new Intent(ACTION_STOP_SERVICE, null, this, SessionService.class); PendingIntent stopServicePendingIntent = PendingIntent.getService(this, 0, stopServiceIntent, 0); notice.addAction(android.R.drawable.ic_menu_close_clear_cancel, exitText, stopServicePendingIntent); notificationManager.notify(NOTIFICATION_ID, notice.build()); } else { notificationManager.notify(NOTIFICATION_ID, notice.getNotification()); } } private void hideNotification() { // hide the notification if we aren't supposed to keep it past the service life if (!keepNotification) notificationManager.cancel(NOTIFICATION_ID); } public void onStateChange(STATE oldState, STATE newState, int resID) { if (newState == STATE.ERROR) stopSelf(); } // Google AppEngine message handler method @Override public void onOpen() { locationHandler.initLocationUpdates(); sensorHandler.initSensors(); // start forwarding sensor data } // Google AppEngine message handler method // Handler for receiving SVMP protocol messages and dispatching them appropriately // Returns true if the message is consumed, false if it is not @Override public boolean onMessage(Response data) { boolean consumed = true; switch (data.getType()) { case AUTH: AuthResponseType type = data.getAuthResponse().getType(); if (type == AuthResponseType.SESSION_MAX_TIMEOUT) { // if we are using the background service preference, change the notification icon to indicate that the connection has been halted boolean useBackground = Utility.getPrefBool(this, R.string.preferenceKey_connection_useBackground, R.string.preferenceValue_connection_useBackground); if (useBackground) { keepNotification = true; showNotification(false); } // the activity isn't running... if (!binder.isBound()) { // clear timed out session information from memory databaseHandler.clearSessionInfo(connectionInfo); // create a toast doToast(R.string.svmpActivity_toast_sessionMaxTimeout); } } case SCREENINFO: case WEBRTC: consumed = false; // pass this message on to the activity message handler break; case LOCATION: locationHandler.handleLocationResponse(data); break; // This is an ACK to the video STOP request. case INTENT: // handler is needed, we might create a toast from a background thread final Response finalData = data; handler.post(new Runnable() { public void run() { IntentHandler.inspect(finalData, SessionService.this); } }); break; case NOTIFICATION: NotificationHandler.inspect(data, SessionService.this, getConnectionID()); break; case PING: long endDate = System.currentTimeMillis(); // immediately get end date if (data.hasPingResponse()) performanceAdapter.setPing(data.getPingResponse().getStartDate(), endDate); break; case APPS: consumed = false; // pass this message on to the activity message handler break; default: Log.e(TAG, "Unexpected protocol message of type " + data.getType().name()); } return consumed; } // used by LocationHandler and SensorHandler to send messages public void sendMessage(SVMPProtocol.Request request) { if (binder != null) binder.sendMessage(request); } // Bridge the SensorEventListener callbacks to the SensorHandler @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { if (getState() == STATE.RUNNING) sensorHandler.onAccuracyChanged(sensor, accuracy); } // Bridge the SensorEventListener callbacks to the SensorHandler @Override public void onSensorChanged(SensorEvent event) { if (getState() == STATE.RUNNING) sensorHandler.onSensorChanged(event); } private void doToast(final int resID) { // handler is needed to create a toast from a background thread handler.post(new Runnable() { public void run() { Toast.makeText(SessionService.this, resID, Toast.LENGTH_LONG).show(); } }); } }