/**
*
* 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;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.util.Log;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.TypeAdapterFactory;
import edu.mit.media.funf.Schedule.BasicSchedule;
import edu.mit.media.funf.Schedule.DefaultSchedule;
import edu.mit.media.funf.action.Action;
import edu.mit.media.funf.config.ConfigUpdater;
import edu.mit.media.funf.config.ConfigurableTypeAdapterFactory;
import edu.mit.media.funf.config.ContextInjectorTypeAdapaterFactory;
import edu.mit.media.funf.config.DefaultRuntimeTypeAdapterFactory;
import edu.mit.media.funf.config.DefaultScheduleSerializer;
import edu.mit.media.funf.config.HttpConfigUpdater;
import edu.mit.media.funf.config.ListenerInjectorTypeAdapterFactory;
import edu.mit.media.funf.config.SingletonTypeAdapterFactory;
import edu.mit.media.funf.datasource.Startable;
import edu.mit.media.funf.datasource.StartableDataSource;
import edu.mit.media.funf.pipeline.Pipeline;
import edu.mit.media.funf.pipeline.PipelineFactory;
import edu.mit.media.funf.probe.Probe;
import edu.mit.media.funf.probe.Probe.DataListener;
import edu.mit.media.funf.storage.DefaultArchive;
import edu.mit.media.funf.storage.FileArchive;
import edu.mit.media.funf.storage.HttpArchive;
import edu.mit.media.funf.storage.RemoteFileArchive;
import edu.mit.media.funf.util.LogUtil;
import edu.mit.media.funf.util.StringUtil;
public class FunfManager extends Service {
public static final String
ACTION_KEEP_ALIVE = "funf.keepalive",
ACTION_INTERNAL = "funf.internal";
private static final String
PIPELINE_TYPE = "funf/pipeline",
ALARM_TYPE = "funf/alarm";
private static final String
DISABLED_PIPELINE_LIST = "__DISABLED__";
private Handler handler;
private SharedPreferences prefs;
private Map<String,Pipeline> pipelines;
private Map<String,Pipeline> disabledPipelines;
private Set<String> disabledPipelineNames;
@Override
public void onCreate() {
super.onCreate();
this.handler = new Handler();
getGson(); // Sets gson
this.prefs = getSharedPreferences(getClass().getName(), MODE_PRIVATE);
this.pipelines = new HashMap<String, Pipeline>();
this.disabledPipelines = new HashMap<String, Pipeline>();
this.disabledPipelineNames = new HashSet<String>(Arrays.asList(prefs.getString(DISABLED_PIPELINE_LIST, "").split(",")));
this.disabledPipelineNames.remove(""); // Remove the empty name, if no disabled pipelines exist
reload();
}
public void reload() {
if (Looper.myLooper() != Looper.getMainLooper()) {
handler.post(new Runnable() {
@Override
public void run() {
reload();
}
});
return;
}
Set<String> pipelineNames = new HashSet<String>();
pipelineNames.addAll(prefs.getAll().keySet());
pipelineNames.remove(DISABLED_PIPELINE_LIST);
Bundle metadata = getMetadata();
pipelineNames.addAll(metadata.keySet());
for (String pipelineName : pipelineNames) {
reload(pipelineName);
}
}
public void reload(final String name) {
if (Looper.myLooper() != Looper.getMainLooper()) {
handler.post(new Runnable() {
@Override
public void run() {
reload(name);
}
});
return;
}
String pipelineConfig = null;
Bundle metadata = getMetadata();
if (prefs.contains(name)) {
pipelineConfig = prefs.getString(name, null);
} else if (metadata.containsKey(name)) {
pipelineConfig = metadata.getString(name);
}
if (disabledPipelineNames.contains(name)) {
// Disabled, so don't load any config
Pipeline disabledPipeline = gson.fromJson(pipelineConfig, Pipeline.class);
disabledPipelines.put(name, disabledPipeline);
pipelineConfig = null;
}
if (pipelineConfig == null) {
unregisterPipeline(name);
} else {
Pipeline newPipeline = gson.fromJson(pipelineConfig, Pipeline.class);
registerPipeline(name, newPipeline); // Will unregister previous before running
}
}
public JsonObject getPipelineConfig(String name) {
String configString = prefs.getString(name, null);
Bundle metadata = getMetadata();
if (configString == null && metadata.containsKey(name)) {
configString = metadata.getString(name);
}
return configString == null ? null : new JsonParser().parse(configString).getAsJsonObject();
}
public boolean save(String name, JsonObject config) {
try {
// Check if this is a valid pipeline before saving
Pipeline pipeline = getGson().fromJson(config, Pipeline.class);
return prefs.edit().putString(name, config.toString()).commit();
} catch (Exception e) {
Log.e(LogUtil.TAG, "Unable to save config: " + config.toString());
return false;
}
}
public boolean saveAndReload(String name, JsonObject config) {
boolean success = save(name, config);
if (success) {
reload(name);
}
return success;
}
@Override
public void onDestroy() {
super.onDestroy();
// TODO: call onDestroy on all pipelines
for (Pipeline pipeline : pipelines.values()) {
pipeline.onDestroy();
}
// TODO: save outstanding requests
// TODO: remove all remaining Alarms
// TODO: make sure to destroy all probes
for (Object probeObject : getProbeFactory().getCached()) {
//String componentString = JsonUtils.immutable(gson.toJsonTree(probeObject)).toString();
//cancelProbe(componentString);
((Probe)probeObject).destroy();
}
getProbeFactory().clearCache();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String action = intent.getAction();
if (action == null || ACTION_KEEP_ALIVE.equals(action)) {
// Does nothing, but wakes up FunfManager
} else if (ACTION_INTERNAL.equals(action)) {
String type = intent.getType();
Uri componentUri = intent.getData();
if (PIPELINE_TYPE.equals(type)) {
// Handle pipeline action
String pipelineName = getComponentName(componentUri);
String pipelineAction = getAction(componentUri);
Pipeline pipeline = pipelines.get(pipelineName);
if (pipeline != null) {
pipeline.onRun(pipelineAction, null);
}
} else if (ALARM_TYPE.equals(type)) {
// Handle registered alarms
String probeConfig = getComponentName(componentUri);
final Probe probe = getGson().fromJson(probeConfig, Probe.class);
if (probe instanceof Runnable) {
handler.post((Runnable)probe);
}
}
}
return Service.START_FLAG_RETRY; // TODO: may want the last intent always redelivered to make sure system starts up
}
private Bundle getMetadata() {
try {
Bundle metadata = getPackageManager().getServiceInfo(new ComponentName(this, this.getClass()), PackageManager.GET_META_DATA).metaData;
return metadata == null ? new Bundle() : metadata;
} catch (NameNotFoundException e) {
throw new RuntimeException("Unable to get metadata for the FunfManager service.");
}
}
/**
* Get a gson builder with the probe factory built in
* @return
*/
public GsonBuilder getGsonBuilder() {
return getGsonBuilder(this);
}
public static class ConfigurableRuntimeTypeAdapterFactory<E> extends DefaultRuntimeTypeAdapterFactory<E> {
public ConfigurableRuntimeTypeAdapterFactory(Context context, Class<E> baseClass, Class<? extends E> defaultClass) {
super(context,
baseClass,
defaultClass,
new ContextInjectorTypeAdapaterFactory(context, new ConfigurableTypeAdapterFactory()));
}
}
/**
* Get a gson builder with the probe factory built in
* @return
*/
public static GsonBuilder getGsonBuilder(Context context) {
return new GsonBuilder()
.registerTypeAdapterFactory(getProbeFactory(context))
.registerTypeAdapterFactory(getActionFactory(context))
.registerTypeAdapterFactory(getPipelineFactory(context))
.registerTypeAdapterFactory(getDataSourceFactory(context))
.registerTypeAdapterFactory(new ConfigurableRuntimeTypeAdapterFactory<Schedule>(context, Schedule.class, BasicSchedule.class))
.registerTypeAdapterFactory(new ConfigurableRuntimeTypeAdapterFactory<ConfigUpdater>(context, ConfigUpdater.class, HttpConfigUpdater.class))
.registerTypeAdapterFactory(new ConfigurableRuntimeTypeAdapterFactory<FileArchive>(context, FileArchive.class, DefaultArchive.class))
.registerTypeAdapterFactory(new ConfigurableRuntimeTypeAdapterFactory<RemoteFileArchive>(context, RemoteFileArchive.class, HttpArchive.class))
.registerTypeAdapterFactory(new ConfigurableRuntimeTypeAdapterFactory<DataListener>(context, DataListener.class, null))
.registerTypeAdapter(DefaultSchedule.class, new DefaultScheduleSerializer())
.registerTypeAdapter(Class.class, new JsonSerializer<Class<?>>() {
@Override
public JsonElement serialize(Class<?> src, Type typeOfSrc, JsonSerializationContext context) {
return src == null ? JsonNull.INSTANCE : new JsonPrimitive(src.getName());
}
});
}
private Gson gson;
/**
* Get a Gson instance which includes the SingletonProbeFactory
* @return
*/
public Gson getGson() {
if (gson == null) {
gson = getGsonBuilder().create();
}
return gson;
}
public TypeAdapterFactory getPipelineFactory() {
return getPipelineFactory(this);
}
private static PipelineFactory PIPELINE_FACTORY;
public static PipelineFactory getPipelineFactory(Context context) {
if (PIPELINE_FACTORY == null) {
PIPELINE_FACTORY = new PipelineFactory(context);
}
return PIPELINE_FACTORY;
}
public SingletonTypeAdapterFactory getProbeFactory() {
return getProbeFactory(this);
}
private static SingletonTypeAdapterFactory PROBE_FACTORY;
public static SingletonTypeAdapterFactory getProbeFactory(Context context) {
if (PROBE_FACTORY == null) {
PROBE_FACTORY = new SingletonTypeAdapterFactory(
new DefaultRuntimeTypeAdapterFactory<Probe>(
context,
Probe.class,
null,
new ContextInjectorTypeAdapaterFactory(context, new ConfigurableTypeAdapterFactory())));
}
return PROBE_FACTORY;
}
public DefaultRuntimeTypeAdapterFactory<Action> getActionFactory() {
return getActionFactory(this);
}
private static DefaultRuntimeTypeAdapterFactory<Action> ACTION_FACTORY;
public static DefaultRuntimeTypeAdapterFactory<Action> getActionFactory(Context context) {
if (ACTION_FACTORY == null) {
ACTION_FACTORY = new DefaultRuntimeTypeAdapterFactory<Action>(
context,
Action.class,
null,
new ContextInjectorTypeAdapaterFactory(context, new ConfigurableTypeAdapterFactory()));
}
return ACTION_FACTORY;
}
public ListenerInjectorTypeAdapterFactory getDataSourceFactory() {
return getDataSourceFactory(this);
}
private static ListenerInjectorTypeAdapterFactory DATASOURCE_FACTORY;
public static ListenerInjectorTypeAdapterFactory getDataSourceFactory(Context context) {
if (DATASOURCE_FACTORY == null) {
DATASOURCE_FACTORY = new ListenerInjectorTypeAdapterFactory(
new DefaultRuntimeTypeAdapterFactory<Startable>(
context,
Startable.class,
null,
new ContextInjectorTypeAdapaterFactory(context, new ConfigurableTypeAdapterFactory())));
}
return DATASOURCE_FACTORY;
}
public void registerPipeline(String name, Pipeline pipeline) {
synchronized (pipelines) {
Log.d(LogUtil.TAG, "Registering pipeline: " + name);
unregisterPipeline(name);
pipelines.put(name, pipeline);
pipeline.onCreate(this);
}
}
public Pipeline getRegisteredPipeline(String name) {
Pipeline p = pipelines.get(name);
if (p == null) {
p = disabledPipelines.get(name);
}
return p;
}
public void unregisterPipeline(String name) {
synchronized (pipelines) {
Pipeline existingPipeline = pipelines.remove(name);
if (existingPipeline != null) {
existingPipeline.onDestroy();
}
}
}
public void enablePipeline(String name) {
boolean previouslyDisabled = disabledPipelineNames.remove(name);
if (previouslyDisabled) {
prefs.edit().putString(DISABLED_PIPELINE_LIST, StringUtil.join(disabledPipelineNames, ",")).commit();
reload(name);
}
}
public boolean isEnabled(String name) {
return this.pipelines.containsKey(name) && !disabledPipelineNames.contains(name);
}
public void disablePipeline(String name) {
boolean previouslyEnabled = disabledPipelineNames.add(name);
if (previouslyEnabled) {
prefs.edit().putString(DISABLED_PIPELINE_LIST, StringUtil.join(disabledPipelineNames, ",")).commit();
reload(name);
}
}
private String getPipelineName(Pipeline pipeline) {
for (Map.Entry<String, Pipeline> entry : pipelines.entrySet()) {
if (entry.getValue() == pipeline) {
return entry.getKey();
}
}
return null;
}
public class LocalBinder extends Binder {
public FunfManager getManager() {
return FunfManager.this;
}
}
@Override
public IBinder onBind(Intent intent) {
return new LocalBinder();
}
public static void registerAlarm(Context context, String probeConfig, Long start, Long interval, boolean exact) {
AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
Intent intent = getFunfIntent(context, ALARM_TYPE, probeConfig, "");
PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
if (start == null)
start = 0L;
if (interval == null || interval <= 0) {
alarmManager.set(AlarmManager.RTC_WAKEUP, start, pendingIntent);
} else {
if (exact) {
alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, start, interval, pendingIntent);
} else {
alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, start, interval, pendingIntent);
}
}
}
public static void unregisterAlarm(Context context, String probeConfig) {
Intent intent = getFunfIntent(context, ALARM_TYPE, probeConfig, "");
PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_NO_CREATE);
if (pendingIntent != null) {
pendingIntent.cancel();
}
}
/////////////////////////////////////////////
// Reserve action for later inter-funf communication
// Use type to differentiate between probe/pipeline
// funf:<componenent_name>#<action>
private static final String
FUNF_SCHEME = "funf";
// TODO: should these public? May be confusing for people just using the library
private static Uri getComponentUri(String component, String action) {
return new Uri.Builder()
.scheme(FUNF_SCHEME)
.path(component) // Automatically prepends slash
.fragment(action)
.build();
}
private static String getComponentName(Uri componentUri) {
return componentUri.getPath().substring(1); // Remove automatically prepended slash from beginning
}
private static String getAction(Uri componentUri) {
return componentUri.getFragment();
}
private static Intent getFunfIntent(Context context, String type, String component, String action) {
return getFunfIntent(context, type, getComponentUri(component, action));
}
private static Intent getFunfIntent(Context context, String type, Uri componentUri) {
Intent intent = new Intent();
intent.setClass(context, FunfManager.class);
intent.setPackage(context.getPackageName());
intent.setAction(ACTION_INTERNAL);
intent.setDataAndType(componentUri, type);
return intent;
}
}