/** * 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.configured; import static edu.mit.media.funf.AsyncSharedPrefs.async; import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Map; import org.json.JSONException; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.util.Log; import edu.mit.media.funf.CustomizedIntentService; import edu.mit.media.funf.FileUtils; import edu.mit.media.funf.IOUtils; import edu.mit.media.funf.Utils; import edu.mit.media.funf.probe2.Probe; import edu.mit.media.funf.storage.BundleSerializer; import edu.mit.media.funf.storage.DatabaseService; import edu.mit.media.funf.storage.DefaultArchive; import edu.mit.media.funf.storage.HttpUploadService; import edu.mit.media.funf.storage.NameValueDatabaseService; import edu.mit.media.funf.storage.NameValueProbeDataListener; import edu.mit.media.funf.storage.UploadService; public abstract class ConfiguredPipeline extends CustomizedIntentService implements OnSharedPreferenceChangeListener { private static final String PREFIX = "edu.mit.media.funf."; public static final String ACTION_RELOAD = PREFIX + "reload", ACTION_UPDATE_CONFIG = PREFIX + "update", ACTION_UPLOAD_DATA = PREFIX + "upload", ACTION_ARCHIVE_DATA = PREFIX + "archive", ACTION_ENABLE = PREFIX + "enable", ACTION_DISABLE = PREFIX + "disable"; public static final String LAST_CONFIG_UPDATE = "LAST_CONFIG_UPDATE"; public static final String LAST_DATA_UPLOAD = "LAST_DATA_UPLOAD"; public static final String CONFIG = "config", CONFIG_URL = "config_url", CONFIG_FILE = "config_file"; private Map<String, Bundle[]> sentProbeRequests = null; private BroadcastReceiver dataListener; private Handler handler; public ConfiguredPipeline() { super("ConfiguredPipeline"); } @Override public void onCreate() { super.onCreate(); handler = new Handler(); ensureServicesAreRunning(); getConfig().getPrefs().registerOnSharedPreferenceChangeListener(this); getSystemPrefs().registerOnSharedPreferenceChangeListener(this); } @Override public void onDestroy() { super.onDestroy(); getConfig().getPrefs().unregisterOnSharedPreferenceChangeListener(this); getSystemPrefs().unregisterOnSharedPreferenceChangeListener(this); } // HACK: Send a fake start id to prevent this service from being stopped // This is so we could use all of the other features of Intent service without rewriting them private static final int FAKE_START_ID = 98723546; @Override public int onStartCommand(Intent intent, int flags, int startId) { return super.onStartCommand(intent, flags, FAKE_START_ID); } @Override protected void onHandleIntent(Intent intent) { String action = intent.getAction(); if (ACTION_RELOAD.equals(action)) { reload(); } else if(ACTION_UPDATE_CONFIG.equals(action)) { String config = intent.getStringExtra(CONFIG); String configUrl = intent.getStringExtra(CONFIG_URL); String configFilePath = intent.getStringExtra(CONFIG_FILE); if (config != null) { updateConfig(config); } else if (configFilePath != null) { File file = new File(configFilePath); updateConfig(file); } else if (configUrl != null) { try { updateConfig(new URL(configUrl)); } catch (MalformedURLException e) { Log.e(TAG, "Unable to parse config url."); } } else { updateConfig(); } } else if(ACTION_UPLOAD_DATA.equals(action)) { uploadData(); } else if(ACTION_ARCHIVE_DATA.equals(action)) { archiveData(); } else if(ACTION_ENABLE.equals(action)) { setEnabled(true); } else if(ACTION_DISABLE.equals(action)) { setEnabled(false); } else if (Probe.ACTION_DATA.equals(action)) { onDataReceived(intent.getExtras()); } else if (Probe.ACTION_STATUS.equals(action)) { onStatusReceived(new Probe.Status(intent.getExtras())); } else if (Probe.ACTION_DETAILS.equals(action)) { onDetailsReceived(new Probe.Details(intent.getExtras())); } } public static final String ENABLED_KEY = "enabled"; public void onSharedPreferenceChanged (SharedPreferences sharedPreferences, String key) { Log.i(TAG, "Shared Prefs changed"); if (sharedPreferences.equals(getConfig().getPrefs())) { Log.i(TAG, "Configuration changed"); onConfigChange(getConfig().toString(true)); if (FunfConfig.isDataRequestKey(key)) { if (isEnabled()) { String probeName = FunfConfig.keyToProbename(key); sendProbeRequest(probeName); } } else if (FunfConfig.CONFIG_UPDATE_PERIOD_KEY.equals(key)) { cancelAlarm(ACTION_UPDATE_CONFIG); } else if (FunfConfig.DATA_ARCHIVE_PERIOD_KEY.equals(key)) { cancelAlarm(ACTION_ARCHIVE_DATA); } else if (FunfConfig.DATA_UPLOAD_PERIOD_KEY.equals(key)) { cancelAlarm(ACTION_UPLOAD_DATA); } if (isEnabled()) { scheduleAlarms(); } } else if (sharedPreferences.equals(getSystemPrefs()) && ENABLED_KEY.equals(key)) { Log.i(TAG, "System prefs changed"); reload(); } } public void reload() { cancelAlarms(); removeProbeRequests(); sentProbeRequests = null; if (isEnabled()) { ensureServicesAreRunning(); } } private void scheduleAlarms() { FunfConfig config = getConfig(); scheduleAlarm(ACTION_UPDATE_CONFIG, config.getConfigUpdatePeriod()); scheduleAlarm(ACTION_ARCHIVE_DATA, config.getDataArchivePeriod()); scheduleAlarm(ACTION_UPLOAD_DATA, config.getDataUploadPeriod()); } private void scheduleAlarm(String action, long delayInSeconds) { Intent i = new Intent(this, getClass()); i.setAction(action); boolean noAlarmExists = (PendingIntent.getService(this, 0, i, PendingIntent.FLAG_NO_CREATE) == null); if (noAlarmExists) { PendingIntent pi = PendingIntent.getService(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT); AlarmManager alarmManager = (AlarmManager)getSystemService(ALARM_SERVICE); long delayInMilliseconds = Utils.secondsToMillis(delayInSeconds); long startTimeInMilliseconds = System.currentTimeMillis() + delayInMilliseconds; Log.i(TAG, "Scheduling alarm for '" + action + "' at " + Utils.millisToSeconds(startTimeInMilliseconds) + " and every " + delayInSeconds + " seconds"); // Inexact repeating doesn't work unlesss interval is 15, 30 min, or 1, 6, or 24 hours alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, startTimeInMilliseconds, delayInMilliseconds, pi); } } private void cancelAlarms() { cancelAlarm(ACTION_UPDATE_CONFIG); cancelAlarm(ACTION_ARCHIVE_DATA); cancelAlarm(ACTION_UPLOAD_DATA); } private void cancelAlarm(String action) { Intent i = new Intent(this, getClass()); i.setAction(action); PendingIntent pi = PendingIntent.getService(this, 0, i, PendingIntent.FLAG_NO_CREATE); if (pi != null) { AlarmManager alarmManager = (AlarmManager)getSystemService(ALARM_SERVICE); alarmManager.cancel(pi); } } public void ensureServicesAreRunning() { if (isEnabled()) { scheduleAlarms(); sendProbeRequests(); } } public void setEncryptionPassword(char[] password) { DefaultArchive.getArchive(this, getPipelineName()).setEncryptionPassword(password); } protected PendingIntent getCallback() { // TODO: Maybe do a callback per probe, so they can be cancelled individually return PendingIntent.getService(this, 0, new Intent(this, getClass()), PendingIntent.FLAG_UPDATE_CURRENT); } public void sendProbeRequests() { Map<String,Bundle[]> dataRequests = getConfig().getDataRequests(); for (String probeName : dataRequests.keySet()) { sendProbeRequest(probeName); } } public void sendProbeRequest(String probeName) { Bundle[] requests = getConfig().getDataRequests().get(probeName); if (requests == null) { requests = new Bundle[] {}; // null is same as blank config } ArrayList<Bundle> dataRequest = new ArrayList<Bundle>(Arrays.asList(requests)); Intent request = new Intent(Probe.ACTION_REQUEST); request.setClassName(this, probeName); request.putExtra(Probe.CALLBACK_KEY, getCallback()); request.putExtra(Probe.REQUESTS_KEY, dataRequest); startService(request); } private void removeProbeRequests() { getCallback().cancel(); } public static final String DEFAULT_PIPELINE_NAME = "mainPipeline"; public String getPipelineName() { return DEFAULT_PIPELINE_NAME; } public void updateConfig() { String configUpdateUrl = getConfig().getConfigUpdateUrl(); if (configUpdateUrl == null) { Log.i(TAG, "No update url configured."); } else { try { updateConfig(new URL(configUpdateUrl)); } catch (MalformedURLException e) { Log.e(TAG, "Invalid update URL specified.", e); } } } public void updateConfig(URL url) { String jsonString = IOUtils.httpGet(url.toExternalForm(), null); updateConfig(jsonString); } private static final long MAX_REASONABLE_CONFIG_FILE_SIZE = (long)(Math.pow(2, 20)); public void updateConfig(File file) { try { String jsonString = FileUtils.getStringFromFileWithLimit(file, MAX_REASONABLE_CONFIG_FILE_SIZE); updateConfig(jsonString); } catch (IllegalArgumentException e) { Log.e(TAG, "Too large to a valid configuration file.", e); } } public void updateConfig(String jsonString) { if (jsonString == null) { Log.e(TAG, "A null configuration cannot be specified."); return; } try { Log.i(TAG, "Updating pipeline config."); // Write to temporary to compare FunfConfig tempConfig = getTemporaryConfig(); boolean successfullyWroteConfig = tempConfig.edit().setAll(jsonString).commit(); if (successfullyWroteConfig) { getSystemPrefs().edit().putLong(LAST_CONFIG_UPDATE, System.currentTimeMillis()).commit(); } if (successfullyWroteConfig && !tempConfig.equals(getConfig())) { getConfig().edit().setAll(tempConfig).commit(); } } catch (JSONException e) { Log.e(TAG, "Unable to update configuration.", e); } } protected void onConfigChange(String json) { // Record configuration change to database Intent i = new Intent(this, getDatabaseServiceClass()); i.setAction(DatabaseService.ACTION_RECORD); i.putExtra(DatabaseService.DATABASE_NAME_KEY, getPipelineName()); i.putExtra(NameValueDatabaseService.TIMESTAMP_KEY, System.currentTimeMillis()); i.putExtra(NameValueDatabaseService.NAME_KEY, getClass().getName()); i.putExtra(NameValueDatabaseService.VALUE_KEY, json); startService(i); } public void uploadData() { archiveData(); String archiveName = getPipelineName(); String uploadUrl = getConfig().getDataUploadUrl(); Intent i = new Intent(this, getUploadServiceClass()); i.putExtra(UploadService.ARCHIVE_ID, archiveName); i.putExtra(UploadService.REMOTE_ARCHIVE_ID, uploadUrl); startService(i); getSystemPrefs().edit().putLong(LAST_DATA_UPLOAD, System.currentTimeMillis()).commit(); } public void archiveData() { Intent i = new Intent(this, getDatabaseServiceClass()); i.setAction(DatabaseService.ACTION_ARCHIVE); i.putExtra(DatabaseService.DATABASE_NAME_KEY, getPipelineName()); startService(i); } public boolean isEnabled() { return getSystemPrefs().getBoolean(ENABLED_KEY, true); } public boolean setEnabled(boolean enabled) { return getSystemPrefs().edit().putBoolean(ENABLED_KEY, enabled).commit(); } public Class<? extends DatabaseService> getDatabaseServiceClass() { return NameValueDatabaseService.class; } public Class<? extends UploadService> getUploadServiceClass() { return HttpUploadService.class; } public BroadcastReceiver getProbeDataListener() { return new NameValueProbeDataListener(getPipelineName(), getDatabaseServiceClass(), getBundleSerializer()); } public void onDataReceived(Bundle data) { String dataJson = getBundleSerializer().serialize(data); String probeName = data.getString(Probe.PROBE); long timestamp = data.getLong(Probe.TIMESTAMP, 0L); Bundle b = new Bundle(); b.putString(NameValueDatabaseService.DATABASE_NAME_KEY, getPipelineName()); b.putLong(NameValueDatabaseService.TIMESTAMP_KEY, timestamp); b.putString(NameValueDatabaseService.NAME_KEY, probeName); b.putString(NameValueDatabaseService.VALUE_KEY, dataJson); Intent i = new Intent(this, getDatabaseServiceClass()); i.setAction(DatabaseService.ACTION_RECORD); i.putExtras(b); startService(i); } public void onStatusReceived(Probe.Status status) { // TODO: } public void onDetailsReceived(Probe.Details details) { // TODO: } // TODO: enable json serialization of bundles to eliminate the need for this class to be abstract public abstract BundleSerializer getBundleSerializer(); public SharedPreferences getSystemPrefs() { return async(getSharedPreferences(getClass().getName() + "_system", MODE_PRIVATE)); } public FunfConfig getConfig() { return getConfig(this, getClass().getName() + "_config"); } protected static FunfConfig getConfig(Context context, String name) { SharedPreferences prefs = context.getSharedPreferences(name, MODE_PRIVATE); return FunfConfig.getInstance(async(prefs)); } /** * Used for testing the configuration before loading it into the actual config * @return */ protected FunfConfig getTemporaryConfig() { SharedPreferences prefs = getSharedPreferences(getClass().getName() + "_tempconfig", MODE_PRIVATE); return FunfConfig.getInstance(async(prefs)); } @Override protected void onEndOfQueue() { // nothing } /** * Binder interface to the probe */ public class LocalBinder extends Binder { public ConfiguredPipeline getService() { return ConfiguredPipeline.this; } } private final IBinder mBinder = new LocalBinder(); @Override public IBinder onBind(Intent intent) { return mBinder; } }