/** * * Funf: Open Sensing Framework * Copyright (C) 2010-2011 Nadav Aharony, Wei Pan, Alex Pentland. * Acknowledgments: Alan Gardner * Contact: nadav@media.mit.edu * * This file is part of Funf. * * Funf is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * Funf is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with Funf. If not, see <http://www.gnu.org/licenses/>. * */ package edu.mit.media.funf.probe; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; import android.content.Context; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.PowerManager; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.TypeAdapterFactory; import edu.mit.media.funf.FunfManager; import edu.mit.media.funf.Schedule.DefaultSchedule; import edu.mit.media.funf.config.Configurable; import edu.mit.media.funf.data.DataNormalizer; import edu.mit.media.funf.json.BundleTypeAdapter; import edu.mit.media.funf.json.IJsonObject; import edu.mit.media.funf.json.JsonUtils; import edu.mit.media.funf.probe.builtin.ProbeKeys.BaseProbeKeys; import edu.mit.media.funf.security.HashUtil; import edu.mit.media.funf.security.HashUtil.HashingType; import edu.mit.media.funf.time.TimeUtil; import edu.mit.media.funf.util.LockUtil; public interface Probe { public static final String DEFAULT_CONFIG = "{}"; public static final boolean DEFAULT_OPPORTUNISTIC = true; public static final boolean DEFAULT_STRICT = false; public static final double DEFAULT_PERIOD = 3600; /** * Listeners added to this probe will receive data callbacks from this * probe, until this listener is unregistered. The probe should continue to * be active and send data to this listener until it is unregistered. This * method is indempotent and thread safe. * * @param listener */ public void registerListener(DataListener... listener); /** * Remove all listeners and disable; */ public void destroy(); /** * @return the current state of the probe. */ public State getState(); public void addStateListener(StateListener listener); public void removeStateListener(StateListener listener); public interface PassiveProbe extends Probe { /** * Listeners removed from this probe will no longer receive data * callbacks from this probe. This method is indempotent and thread * safe. * * @param listener */ public void registerPassiveListener(DataListener... listener); /** * Listeners removed from this probe will no longer receive data * callbacks from this probe. This method is indempotent and thread * safe. * * @param listener */ public void unregisterPassiveListener(DataListener... listener); } public interface ContinuousProbe extends Probe { public static final double DEFAULT_DURATION = 60; // One minute /** * Listeners removed from this probe will no longer receive data * callbacks from this probe. This method is indempotent and thread * safe. * * @param listener */ public void unregisterListener(DataListener... listener); } /** * A probe that can continue where it left off, using a checkpoint. * */ public interface ContinuableProbe extends Probe { /** * @return The checkpoint that represents the state of the data stream * at the point this is called. */ public JsonElement getCheckpoint(); /** * Sets the checkpoint the probe should start the sending data from. * Like setConfig, calling this will disable the probe. * * @param checkpoint */ public void setCheckpoint(JsonElement checkpoint); } @Documented @Retention(RUNTIME) @Target(TYPE) @Inherited public @interface DisplayName { String value(); } @Documented @Retention(RUNTIME) @Target(TYPE) @Inherited public @interface Description { String value(); } @Documented @Retention(RUNTIME) @Target(TYPE) @Inherited public @interface RequiredPermissions { String[] value(); } @Documented @Retention(RUNTIME) @Target(TYPE) @Inherited public @interface RequiredFeatures { String[] value(); } @Documented @Retention(RUNTIME) @Target(TYPE) @Inherited public @interface RequiredProbes { Class<? extends Probe>[] value(); } /** * Interface implemented by Probe data observers. */ public interface DataListener { /** * Called when the probe emits data. Data emitted from probes that * extend the Probe class are guaranteed to have the PROBE and TIMESTAMP * parameters. * * @param data */ public void onDataReceived(IJsonObject probeConfig, IJsonObject data); /** * Called when the probe is finished sending a stream of data. This can * be used to know when the probe was run, even if it didn't send data. * It can also be used to get a checkpoint of far through the data * stream the probe ran. Continuable probes can use this checkpoint to * start the data stream where it previously left off. * * @param completeProbeUri * @param checkpoint */ public void onDataCompleted(IJsonObject probeConfig, JsonElement checkpoint); } /** * Interface implemented by Probe status observers. */ public interface StateListener { /** * Called when the probe emits a status message, which can happen when * the probe changes state. * * @param status */ public void onStateChanged(Probe probe, State previousState); } /** * Types to represent the current state of the probe. Provides the * implementation of the ProbeRunnable state machine. * */ public static enum State { // TODO: should we try catch, to prevent one probe from killing all // probes? DISABLED { @Override protected void enable(Base probe) { synchronized (probe) { probe.state = ENABLED; probe.onEnable(); probe.notifyStateChange(this); } } @Override protected void start(Base probe) { synchronized (probe) { enable(probe); if (probe.state == ENABLED) { ENABLED.start(probe); } } } @Override protected void stop(Base probe) { // Nothing } @Override protected void disable(Base probe) { // Nothing } }, ENABLED { @Override protected void enable(Base probe) { // Nothing } @Override protected void start(Base probe) { synchronized (probe) { if (probe.isWakeLockedWhileRunning()) { JsonElement el = JsonUtils.immutable(probe.getGson().toJsonTree(probe)); probe.lock = LockUtil.getWakeLock(probe.getContext(), el.toString()); } probe.state = RUNNING; probe.onStart(); probe.notifyStateChange(this); } } @Override protected void stop(Base probe) { // Nothing } @Override protected void disable(Base probe) { synchronized (probe) { probe.state = DISABLED; probe.onDisable(); probe.notifyStateChange(this); probe.passiveDataListeners.clear(); probe.dataListeners.clear(); // Shutdown handler thread probe.looper.quit(); probe.looper = null; probe.handler = null; } } }, RUNNING { @Override protected void enable(Base probe) { // Nothing } @Override protected void start(Base probe) { // Nothing } @Override protected void stop(Base probe) { synchronized (probe) { probe.state = ENABLED; probe.onStop(); probe.notifyStateChange(this); probe.unregisterAllListeners(); if (probe.lock != null && probe.lock.isHeld()) { probe.lock.release(); probe.lock = null; } } } @Override protected void disable(Base probe) { synchronized (probe) { stop(probe); if (probe.state == ENABLED) { ENABLED.disable(probe); } } } }; protected abstract void enable(Base probe); protected abstract void disable(Base probe); protected abstract void start(Base probe); protected abstract void stop(Base probe); } @DefaultSchedule public abstract class Base implements Probe, BaseProbeKeys { private Context context; /** * No argument constructor requires that setContext be called manually. */ public Base() { state = State.DISABLED; } public Base(Context context) { this(); this.context = context; } private Gson gson; protected Gson getGson() { if (gson == null) { gson = getGsonBuilder().create(); } return gson; } protected GsonBuilder getGsonBuilder() { GsonBuilder builder = new GsonBuilder(); builder.registerTypeAdapterFactory(FunfManager.getProbeFactory(getContext())); TypeAdapterFactory adapterFactory = getSerializationFactory(); if (adapterFactory != null) { builder.registerTypeAdapterFactory(adapterFactory); } builder.registerTypeAdapterFactory(BundleTypeAdapter.FACTORY); return builder; } // TODO: figure out how to get scheduler to use source data requests to // schedule appropriately // Probably will need to prototype with ActivityProbe @Deprecated public Map<String, JsonObject> getSourceDataRequests() { return null; } private IJsonObject config; public IJsonObject getConfig() { if (config == null) { config = new IJsonObject(getGson().toJsonTree(this).getAsJsonObject()); } return config; } protected Context getContext() { return context; } /***************************************** * Probe Data Listeners *****************************************/ private Set<DataListener> dataListeners = Collections.synchronizedSet(new HashSet<DataListener>()); private Set<DataListener> passiveDataListeners = Collections.synchronizedSet(new HashSet<DataListener>()); /** * Returns the set of data listeners. Make sure to synchronize on this * object, if you plan to modify it or iterate over it. */ protected Set<DataListener> getDataListeners() { return dataListeners; } /** * Returns the set of passive data listeners. Make sure to synchronize * on this object, if you plan to modify it or iterate over it. */ protected Set<DataListener> getPassiveDataListeners() { return dataListeners; } @Override public void registerListener(DataListener... listeners) { if (listeners != null) { for (DataListener listener : listeners) { dataListeners.add(listener); } start(); } } private JsonElement getCheckpointIfContinuable() { JsonElement checkpoint = null; if (this instanceof ContinuableProbe) { checkpoint = ((ContinuableProbe) this).getCheckpoint(); } return checkpoint; } public void unregisterListener(DataListener... listeners) { if (listeners != null) { JsonElement checkpoint = getCheckpointIfContinuable(); for (DataListener listener : listeners) { dataListeners.remove(listener); listener.onDataCompleted(getConfig(), checkpoint); } // If no one is listening, stop using device resources if (dataListeners.isEmpty()) { stop(); } if (passiveDataListeners.isEmpty()) { disable(); } } } protected void unregisterAllListeners() { DataListener[] listeners = null; synchronized (dataListeners) { listeners = new DataListener[dataListeners.size()]; dataListeners.toArray(listeners); } unregisterListener(listeners); } public void registerPassiveListener(DataListener... listeners) { if (listeners != null) { for (DataListener listener : listeners) { dataListeners.add(listener); } enable(); } } public void unregisterPassiveListener(DataListener... listeners) { if (listeners != null) { JsonElement checkpoint = getCheckpointIfContinuable(); for (DataListener listener : listeners) { dataListeners.remove(listener); listener.onDataCompleted(getConfig(), checkpoint); } // If no one is listening, stop using device resources if (dataListeners.isEmpty() && passiveDataListeners.isEmpty()) { disable(); } } } protected void unregisterAllPassiveListeners() { synchronized (passiveDataListeners) { DataListener[] listeners = new DataListener[passiveDataListeners.size()]; passiveDataListeners.toArray(listeners); unregisterPassiveListener(listeners); } } protected void notifyStateChange(State previousState) { synchronized (stateListeners) { for (StateListener listener : stateListeners) { listener.onStateChanged(this, previousState); } } } protected void sendData(final JsonObject data) { if (data == null || looper == null) { return; } else if (Thread.currentThread() != looper.getThread()) { // Ensure the data send runs on the probe's thread if (handler != null) { Message dataMessage = handler.obtainMessage(SEND_DATA_MESSAGE, data); handler.sendMessage(dataMessage); } } else { if (!data.has(TIMESTAMP)) { data.addProperty(TIMESTAMP, TimeUtil.getTimestamp()); } IJsonObject immutableData = new IJsonObject(data); synchronized (dataListeners) { for (DataListener listener : dataListeners) { listener.onDataReceived(getConfig(), immutableData); } } synchronized (passiveDataListeners) { for (DataListener listener : passiveDataListeners) { if (!dataListeners.contains(listener)) { // Don't send // data // twice to // passive // listeners listener.onDataReceived(getConfig(), immutableData); } } } } } /***************************************** * Probe State Machine *****************************************/ private State state; private PowerManager.WakeLock lock; @Override public State getState() { return state; } private void ensureLooperThreadExists() { if (looper == null) { synchronized (this) { if (looper == null) { HandlerThread thread = new HandlerThread("Probe[" + getClass().getName() + "]"); thread.start(); looper = thread.getLooper(); handler = new Handler(looper, new ProbeHandlerCallback()); } } } } protected final void enable() { ensureLooperThreadExists(); handler.sendMessage(handler.obtainMessage(ENABLE_MESSAGE)); } protected final void start() { ensureLooperThreadExists(); handler.sendMessage(handler.obtainMessage(START_MESSAGE)); } protected final void stop() { ensureLooperThreadExists(); handler.sendMessage(handler.obtainMessage(STOP_MESSAGE)); } protected final void disable() { if (handler != null) { handler.sendMessage(handler.obtainMessage(DISABLE_MESSAGE)); } } public void destroy() { disable(); } /** * Called when the probe switches from the disabled to the enabled * state. This is where any passive or opportunistic listeners should be * configured. An enabled probe should not keep a wake lock. If you need * the device to stay awake consider implementing a StartableProbe, and * using the onStart method. */ protected void onEnable() { } /** * Called when the probe switches from the enabled state to active * running state. This should be used to send any data broadcasts, but * must return quickly. If you have any long running processes they * should be started on a separate thread created by this method, or * should be divided into short runnables that are posted to this * threads looper one at a time, to allow for the probe to change state. */ protected void onStart() { } /** * Called with the probe switches from the running state to the enabled * state. This method should be used to stop any running threads * emitting data, or remove a runnable that has been posted to this * thread's looper. Any passive listeners should continue running. */ protected void onStop() { } /** * Called with the probe switches from the enabled state to the disabled * state. This method should be used to stop any passive listeners * created in the onEnable method. This is the time to cleanup and * release any resources before the probe is destroyed. */ protected void onDisable() { } private volatile Looper looper; private volatile Handler handler; /** * Access to the probe thread's handler. * * @return */ protected Handler getHandler() { return handler; } /** * @param msg * @return */ protected boolean handleMessage(Message msg) { // For right now don't handle any messages, only runnables return false; } protected static final int ENABLE_MESSAGE = 1, START_MESSAGE = 2, STOP_MESSAGE = 3, DISABLE_MESSAGE = 4, SEND_DATA_MESSAGE = 5, SEND_DATA_COMPLETE_MESSAGE = 6; private class ProbeHandlerCallback implements Handler.Callback { @Override public boolean handleMessage(Message msg) { switch (msg.what) { case ENABLE_MESSAGE: state.enable(Base.this); break; case START_MESSAGE: state.start(Base.this); break; case STOP_MESSAGE: state.stop(Base.this); break; case DISABLE_MESSAGE: state.disable(Base.this); break; case SEND_DATA_MESSAGE: if (msg.obj instanceof JsonObject) { sendData((JsonObject) msg.obj); } break; case SEND_DATA_COMPLETE_MESSAGE: if (msg.obj instanceof JsonObject) { sendData((JsonObject) msg.obj); } break; default: return Base.this.handleMessage(msg); } return true; // Message was handled } } /***************************************** * Probe State Listeners *****************************************/ private Set<StateListener> stateListeners = Collections.synchronizedSet(new HashSet<StateListener>()); /** * Returns the set of status listeners. Make sure to synchronize on this * object, if you plan to modify it or iterate over it. */ protected Set<StateListener> getStateListeners() { return stateListeners; } @Override public void addStateListener(StateListener listener) { stateListeners.add(listener); } @Override public void removeStateListener(StateListener listener) { stateListeners.remove(listener); } /********************************** * Sensitive Data ********************************/ /** * Sensitive data is hidden by default * This can not be changed by configuration alone * If you uncomment this line please submit the change * to funf@media.mit.edu in accordance with the LGPL license. * */ @Configurable private boolean hideSensitiveData = true; protected final String sensitiveData(String data) { return sensitiveData(data, null); } protected final String sensitiveData(String data, DataNormalizer<String> normalizer) { if (hideSensitiveData) { if (normalizer != null) { data = normalizer.normalize(data); } return HashUtil.hashString(getContext(), data, HashingType.ONE_WAY_HASH); } else { return data; } } /********************************** * Custom serialization ********************************/ /** * Used to override the serialiazation technique for multiple types * * @return */ protected TypeAdapterFactory getSerializationFactory() { return null; } protected boolean isWakeLockedWhileRunning() { return true; } } }