package interdroid.swan.engine; import interdroid.swan.ExpressionManager; import interdroid.swan.R; import interdroid.swan.SensorConfigurationException; import interdroid.swan.SwanException; import interdroid.swan.crossdevice.Converter; import interdroid.swan.crossdevice.Pusher; import interdroid.swan.sensors.SensorInterface; import interdroid.swan.swansong.Expression; import interdroid.swan.swansong.ExpressionFactory; import interdroid.swan.swansong.Result; import interdroid.swan.swansong.ValueExpression; import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.PriorityQueue; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; import android.os.IBinder; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; public class EvaluationEngineService extends Service { private static final String TAG = "EvaluationEngine"; private static final String DATABASE_NAME = "swan"; private static final String TABLE = "expressions"; private static final int DATABASE_VERSION = 1; private static final int NOTIFICATION_ID = 1; public static final String ACTION_REGISTER_REMOTE = "interdroid.swan.register_remote"; public static final String ACTION_UNREGISTER_REMOTE = "interdroid.swan.unregister_remote"; public static final String ACTION_NEW_RESULT_REMOTE = "interdroid.swan.new_result_remote"; public static final String UPDATE_EXPRESSIONS = "interdroid.swan.UPDATE_EXPRESSIONS"; public static final String UPDATE_SENSORS = "interdroid.swan.UPDATE_SENSORS"; @Override public IBinder onBind(Intent intent) { return null; } PriorityQueue<QueuedExpression> mEvaluationQueue = new PriorityQueue<QueuedExpression>(); /** The context expressions, mapped by id. */ private final HashMap<String, QueuedExpression> mRegisteredExpressions = new HashMap<String, QueuedExpression>() { /** * */ private static final long serialVersionUID = -658408645837738007L; @Override public QueuedExpression remove(final Object id) { removeFromDb((String) id); return super.remove(id); } @Override public QueuedExpression put(final String key, final QueuedExpression value) { storeToDb(value); return super.put(key, value); } }; Thread mEvaluationThread = new Thread() { public void run() { while (!interrupted()) { QueuedExpression head = mEvaluationQueue.peek(); if (head == null) { Log.d(TAG, "Nothing to evaluate!"); synchronized (mEvaluationThread) { try { mEvaluationThread.wait(); } catch (InterruptedException e) { continue; } } } else { long deferUntil = head.getDeferUntil(); if (deferUntil <= System.currentTimeMillis()) { // evaluate now try { // evaluation delay is the time in ms between when // the expression should be evaluated (as indicated // by deferuntil) and when it is really evaluated. // Normally the evaluation delay is neglectable, but // when the load is high, this can become // significant. long evaluationDelay; if (deferUntil != 0) { evaluationDelay = System.currentTimeMillis() - deferUntil; // code below for debugging purposes if (evaluationDelay > 3600000) { throw new RuntimeException( "Weird evaluation delay: " + evaluationDelay + ", " + deferUntil); } } else { evaluationDelay = 0; } long start = System.currentTimeMillis(); Result result = mEvaluationManager.evaluate( head.getId(), head.getExpression(), System.currentTimeMillis()); long end = System.currentTimeMillis(); // update with statistics: evaluationTime and evaluationDelay head.evaluated((end - start), evaluationDelay); if (head.update(result)) { Log.d(TAG, "Result: " + result); sendUpdate(head, result); } // re add the expression to the queue synchronized (mEvaluationThread) { mEvaluationQueue.remove(head); mEvaluationQueue.add(head); } } catch (SwanException e) { Log.d(TAG, "Failed to evaluate", e); } } else { synchronized (mEvaluationThread) { try { long waitTime = Math.max( 1, head.getDeferUntil() - System.currentTimeMillis()); // Log.d(TAG, "Waiting for " + waitTime + // " ms."); mEvaluationThread.wait(waitTime); // Log.d(TAG, "Done waiting for " + waitTime // + " ms."); } catch (InterruptedException e) { continue; } } } } } } }; NotificationManager mNotificationManager; Notification mNotification; EvaluationManager mEvaluationManager; /** * @return all expressions saved in the database. */ private void restoreAfterBoot() { SQLiteDatabase db = openDb(); try { Cursor c = db.query(TABLE, new String[] { "expression_id", "expression", "on_true", "on_false", "on_undefined", "on_new_values" }, null, null, null, null, null); if (c != null) { try { if (c.getCount() > 0) { while (c.moveToNext()) { try { String expressionId = c.getString(0); Expression expression = ExpressionFactory .parse(c.getString(1)); Intent onTrue = null; if (c.getString(2) != null) { onTrue = Intent.parseUri(c.getString(2), 0); } Intent onFalse = null; if (c.getString(3) != null) { onFalse = Intent .parseUri(c.getString(3), 0); } Intent onUndefined = null; if (c.getString(4) != null) { onUndefined = Intent.parseUri( c.getString(4), 0); } Intent onNewValues = null; if (c.getString(5) != null) { onNewValues = Intent.parseUri( c.getString(5), 0); } doRegister(expressionId, expression, onTrue, onFalse, onUndefined, onNewValues); } catch (Exception e) { Log.e(TAG, "Error while restoring after boot.", e); } } } } finally { try { c.close(); } catch (Exception e) { Log.w(TAG, "Got exception closing cursor.", e); } } } } finally { closeDb(db); } if (mRegisteredExpressions.size() == 0) { // that means, we tried to restore, but there is nothing active Log.d(TAG, "nothing to restore, shutting down..."); stopSelf(); } } /** * Delete's an expression from the database. * * @param key * The id for the expression. * @param type * The type being removed. */ private void removeFromDb(final String id) { SQLiteDatabase db = openDb(); try { db.execSQL("DELETE FROM " + TABLE + " WHERE expression_id = ?", new String[] { id }); } finally { closeDb(db); } } /** * Closes the expression database. */ private void closeDb(final SQLiteDatabase db) { if (db != null) { db.close(); } } /** * @return an open database for expressions. */ private synchronized SQLiteDatabase openDb() { File dbDir = getDir("databases", Context.MODE_PRIVATE); Log.d(TAG, "Created db dir: " + dbDir); SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(new File(dbDir, DATABASE_NAME), null); Log.d(TAG, "Got database version: " + db.getVersion()); if (db.getVersion() < DATABASE_VERSION) { Log.d(TAG, "Creating table: " + TABLE); db.execSQL("DROP TABLE IF EXISTS " + TABLE); db.execSQL("CREATE TABLE " + TABLE + " (_id integer primary key autoincrement, expression_id string, expression string, on_true string, on_false string, on_undefined string, on_new_values string)"); db.setVersion(DATABASE_VERSION); } return db; } /** * Stores an expression to the database. * * @param key * the key for the expression * @param value * the expression * @param type * the type being stored */ private void storeToDb(final QueuedExpression queued) { SQLiteDatabase db = openDb(); try { // Make sure it doesn't exist first in case we are reloading it. db.delete(TABLE, "expression_id=?", new String[] { queued.getId() }); ContentValues values = new ContentValues(); values.put("expression_id", queued.getId()); values.put("expression", queued.getExpression().toParseString()); if (queued.getOnTrue() != null) { values.put("on_true", queued.getOnTrue().toUri(0)); } if (queued.getOnFalse() != null) { values.put("on_false", queued.getOnFalse().toUri(0)); } if (queued.getOnUndefined() != null) { values.put("on_undefined", queued.getOnUndefined().toUri(0)); } if (queued.getOnNewValues() != null) { values.put("on_new_values", queued.getOnNewValues().toUri(0)); } db.insert(TABLE, "expression_id", values); } finally { closeDb(db); } } @Override public int onStartCommand(Intent intent, int flags, int startId) { // we can get several actions here, both from the API and from the // Sensors as well as from the Boot event if (intent == null) { Log.d(TAG, "huh? intent is null! This should never happen!! We will try to restore..."); restoreAfterBoot(); return START_STICKY; } String action = intent.getAction(); if (ExpressionManager.ACTION_REGISTER.equals(action)) { String id = intent.getStringExtra("expressionId"); try { Expression expression = ExpressionFactory.parse(intent .getStringExtra("expression")); Intent onTrue = intent.getParcelableExtra("onTrue"); Intent onFalse = intent.getParcelableExtra("onFalse"); Intent onUndefined = intent.getParcelableExtra("onUndefined"); Intent onNewValues = intent.getParcelableExtra("onNewValues"); doRegister(id, expression, onTrue, onFalse, onUndefined, onNewValues); } catch (Throwable t) { Log.d(TAG, "Failed to register expression: " + intent.getStringExtra("expression"), t); } } else if (ExpressionManager.ACTION_UNREGISTER.equals(action)) { String id = intent.getStringExtra("expressionId"); doUnregister(id); } else if (ACTION_REGISTER_REMOTE.equals(action)) { Log.d(TAG, "Got remote registration"); Bundle extras = intent.getExtras(); String regId = extras.getString("source"); String expId = extras.getString("id"); String expressionString = extras.getString("data"); try { Expression expression = ExpressionFactory .parse(expressionString); doRegister(regId + Expression.SEPARATOR + expId, expression, null, null, null, null); } catch (Throwable t) { Log.d(TAG, "Failed to register remote expression: " + expressionString, t); } } else if (ACTION_UNREGISTER_REMOTE.equals(action)) { Bundle extras = intent.getExtras(); String regId = extras.getString("source"); String expId = extras.getString("id"); doUnregister(regId + Expression.SEPARATOR + expId); } else if (ACTION_NEW_RESULT_REMOTE.equals(action)) { Bundle extras = intent.getExtras(); String id = extras.getString("id"); Result result = null; try { result = (Result) Converter.stringToObject(extras .getString("data")); } catch (Exception e) { // should not happen throw new RuntimeException("Should not happen. Please debug!"); } mEvaluationManager.newRemoteResult(id, result); doNotify(new String[] { id }); return START_STICKY; } else if (SensorInterface.ACTION_NOTIFY.equals(action)) { String[] ids = intent.getStringArrayExtra("expressionIds"); doNotify(ids); return START_STICKY; } else if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { restoreAfterBoot(); } else if (UPDATE_EXPRESSIONS.equals(action)) { // Use local broadcast manager because broadcast // needs only be send to this local app not other applications on // android LocalBroadcastManager.getInstance(this).sendBroadcast( getRegisteredExpressions()); return START_STICKY; } else if (UPDATE_SENSORS.equals(action)) { LocalBroadcastManager.getInstance(this).sendBroadcast( getActiveSensors()); return START_STICKY; } // after we handled the intent, we should update the notification and // the mode (foreground/background) if (mRegisteredExpressions.size() > 0) { updateNotification(); startForeground(NOTIFICATION_ID, mNotification); } else { stopForeground(true); } return START_STICKY; } private Intent getActiveSensors() { Intent intent = new Intent(UPDATE_SENSORS); intent.putExtra("sensors", mEvaluationManager.activeSensorsAsBundle()); return intent; } private void doRegister(final String id, final Expression expression, final Intent onTrue, final Intent onFalse, final Intent onUndefined, Intent onNewValues) { // handle registration Log.d(TAG, "registring id: " + id + ", expression: " + expression); if (mRegisteredExpressions.containsKey(id)) { // FAIL! Log.d(TAG, "failed to register, already contains id!"); return; } try { mEvaluationManager.initialize(id, expression); } catch (SensorConfigurationException e) { // FAIL! e.printStackTrace(); return; } catch (SensorSetupFailedException e) { // FAIL! e.printStackTrace(); return; } synchronized (mEvaluationThread) { // add this expression to our registered expression, the queue and // notify the evaluation thread QueuedExpression queued = new QueuedExpression(id, expression, onTrue, onFalse, onUndefined, onNewValues); mRegisteredExpressions.put(id, queued); mEvaluationQueue.add(queued); mEvaluationThread.notify(); LocalBroadcastManager.getInstance(this).sendBroadcast( getRegisteredExpressions()); } } private Intent getRegisteredExpressions() { Intent intent = new Intent(UPDATE_EXPRESSIONS); Bundle[] expressions = new Bundle[mRegisteredExpressions.size()]; int i = 0; for (String key : mRegisteredExpressions.keySet()) { expressions[i] = mRegisteredExpressions.get(key).toBundle(); i++; } intent.putExtra("expressions", expressions); return intent; } private void doUnregister(final String id) { QueuedExpression expression = mRegisteredExpressions.get(id); if (expression == null) { // FAIL! Log.d(TAG, "Got spurious unregister for id: " + id); return; } // first stop evaluating synchronized (mEvaluationThread) { mRegisteredExpressions.remove(id); mEvaluationQueue.remove(expression); // do we really need to notify the evaluation thread here? mEvaluationThread.notify(); LocalBroadcastManager.getInstance(this).sendBroadcast( getRegisteredExpressions()); } // then stop sensing mEvaluationManager.stop(id, expression.getExpression()); } // what we get back here are leaf ids of expressions. private void doNotify(String[] ids) { if (ids == null) { return; } for (String id : ids) { String rootId = getRootId(id); QueuedExpression queued = mRegisteredExpressions.get(rootId); if (queued == null) { // TODO: maybe broadcast a message to inform sensors to stop // producing values for the id Log.d(TAG, "Got notify, but no expression registered with id: " + rootId + " (original id: " + id + "), should we kill the sensor?"); continue; } // Log.d(TAG, "Got notification for: " + queued); if (queued.getExpression() instanceof ValueExpression || !queued.isDeferUntilGuaranteed()) { // evaluate now! synchronized (mEvaluationThread) { // get it out the queue, update defer until, and put it // back, then notify the evaluation thread. mEvaluationQueue.remove(queued); mEvaluationManager.clearCacheFor(id); mEvaluationQueue.add(queued); mEvaluationThread.notifyAll(); } } } } /* * (non-Javadoc) * * @see android.app.Service#onCreate() */ @SuppressWarnings("deprecation") @Override public final void onCreate() { super.onCreate(); // construct the sensor manager mEvaluationManager = new EvaluationManager(this); // kick off the evaluation thread mEvaluationThread.start(); // init the notification stuff mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); mNotification = new Notification(R.drawable.ic_stat_swan, "Swan active", System.currentTimeMillis()); mNotification.flags |= Notification.FLAG_ONGOING_EVENT; mNotification.flags |= Notification.FLAG_NO_CLEAR; } @Override public void onDestroy() { mEvaluationManager.destroyAll(); mEvaluationThread.interrupt(); super.onDestroy(); } /** * Update notification. */ @SuppressWarnings("deprecation") private void updateNotification() { Intent notificationIntent = new Intent(this, ExpressionViewerActivity.class); PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0); boolean hasRemote = false; for (String id : mRegisteredExpressions.keySet()) { if (id.contains(Expression.SEPARATOR)) { hasRemote = true; break; } } mNotification.icon = hasRemote ? R.drawable.ic_stat_swan_warning : R.drawable.ic_stat_swan; mNotification.setLatestEventInfo(this, "Swan", "number of expressions: " + mRegisteredExpressions.size(), contentIntent); mNotificationManager.notify(NOTIFICATION_ID, mNotification); } private void sendUpdate(QueuedExpression queued, Result result) { // we know it has changed if (queued.getId().contains(Expression.SEPARATOR)) { sendUpdateToRemote(queued.getId().split(Expression.SEPARATOR)[0], queued.getId().split(Expression.SEPARATOR)[1], result); return; } Intent update = queued.getIntent(result); if (update == null) { Log.d(TAG, "State change, but no update intent defined"); return; } if (queued.getExpression() instanceof ValueExpression) { if (result.getValues() == null) { Log.d(TAG, "Update canceled, no values"); return; } update.putExtra(ExpressionManager.EXTRA_NEW_VALUES, result.getValues()); } else { update.putExtra(ExpressionManager.EXTRA_NEW_TRISTATE, result .getTriState().name()); update.putExtra(ExpressionManager.EXTRA_NEW_TRISTATE_TIMESTAMP, result.getTimestamp()); } try { String intentType = update .getStringExtra(ExpressionManager.EXTRA_INTENT_TYPE); if (intentType == null || intentType .equals(ExpressionManager.INTENT_TYPE_BROADCAST)) { sendBroadcast(update); } else if (intentType .equals(ExpressionManager.INTENT_TYPE_ACTIVITY)) { update.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(update); } else if (intentType.equals(ExpressionManager.INTENT_TYPE_SERVICE)) { startService(update); } } catch (Exception e) { e.printStackTrace(); } } private void sendUpdateToRemote(final String registrationId, final String expressionId, final Result result) { // pusher is async try { Pusher.push(registrationId, expressionId, ACTION_NEW_RESULT_REMOTE, Converter.objectToString(result)); } catch (IOException e) { Log.d(TAG, "Exception in converting result to string", e); } } // helper function to strip the suffixes for an expression generated by the // evaluation engine and retrieve the original user id (the root id) private String getRootId(String id) { for (String suffix : Expression.RESERVED_SUFFIXES) { if (id.endsWith(suffix)) { return getRootId(id.substring(0, id.length() - suffix.length())); } } return id; } }