edu.mit.media.funf.configured.ConfiguredPipeline.java Source code

Java tutorial

Introduction

Here is the source code for edu.mit.media.funf.configured.ConfiguredPipeline.java

Source

/**
 * 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;
    }
}