/**
* 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 edu.mit.media.funf.Utils.nonNullStrings;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.PowerManager;
import android.util.Log;
import edu.mit.media.funf.CustomizedIntentService;
import edu.mit.media.funf.Utils;
import edu.mit.media.funf.probe.Probe.Parameter.Builtin;
import edu.mit.media.funf.probe.ProbeExceptions.UnstorableTypeException;
import edu.mit.media.funf.probe.builtin.ProbeKeys.BaseProbeKeys;
public abstract class Probe extends CustomizedIntentService implements BaseProbeKeys {
protected final String TAG = getClass().getName();
private static final String PREFIX = Probe.class.getName();
public static final String
CALLBACK_KEY = "CALLBACK",
REQUESTS_KEY = "REQUESTS",
ACTION_SEND_DETAILS = PREFIX + ".SEND_DETAILS",
ACTION_SEND_CONFIGURATION = PREFIX + ".SEND_CONFIGURATION",
ACTION_SEND_STATUS = PREFIX + ".SEND_STATUS",
ACTION_REQUEST = PREFIX + ".REQUEST",
ACTION_DATA = PREFIX + ".DATA",
ACTION_DETAILS = PREFIX + ".DETAILS",
ACTION_STATUS = PREFIX + ".STATUS";
private static final String
MOST_RECENT_RUN_KEY = "mostRecentTimeRun",
MOST_RECENT_DATA_KEY = "mostRecentTimeDataSent",
MOST_RECENT_DATA_BY_REQUEST_KEY = "mostRecentTimeDataSentByRequest",
MOST_RECENT_PARAMS_KEY = "mostRecentParamsSent",
NEXT_RUN_TIME_KEY = "nextRunTime";
static final String
ACTION_INTERNAL_REQUESTS = "PROBE_ACTION_INTERNAL_REQUESTS_INTENT",
ACTION_INTERNAL_CALLBACK_REGISTERED = "PROBE_INTERNAL_CALLBACK_REGISTERED",
ACTION_RUN = "PROBE_INTERNAL_RUN",
ACTION_STOP = "PROBE_INTERNAL_STOP",
ACTION_DISABLE = "PROBE_INTERNAL_DISABLE",
INTERNAL_CALLBACK_INTENT = "PROBE_INTERNAL_CALLBACK_INTENT",
INTERNAL_REQUESTS_KEY = "PROBE_INTERNAL_REQUESTS",
INTERNAL_PROBE_STATE = "PROBE_INTERNAL_STATE",
PROBE_STATE_RUNNING = "RUNNING",
PROBE_STATE_ENABLED = "ENABLED",
PROBE_STATE_DISABLED = "DISABLED";
private static final long FAR_IN_FUTURE_MILLIS = 365L * 24 * 60 * 60 * 1000; // One year
private PowerManager.WakeLock lock;
private Intent requestsIntent;
private boolean enabled;
private boolean running;
private SharedPreferences historyPrefs;
private Queue<Intent> pendingRequests;
private Queue<Intent> deadRequests;
@Override
public final void onCreate() {
Log.v(TAG, "CREATED");
super.onCreate();
enabled = false;
running = false;
pendingRequests = new ConcurrentLinkedQueue<Intent>();
deadRequests = new ConcurrentLinkedQueue<Intent>();
}
@Override
public final void onDestroy() {
super.onDestroy();
Log.v(TAG, "DESTROYED");
}
@Override
public void onBeforeDestroy() {
// Ensure disable happens on message thread
disable();
}
private Intent getRequestsIntent() {
Intent i = new Intent(ACTION_INTERNAL_REQUESTS, null, this, getClass());
// TODO: implement security number so that we can verify this was our intent we created, and not an external attack
return i;
}
private boolean isRequestsIntent(Intent intent) {
// TODO: verify security number
// TODO: maybe count brute force attacks, shut down if too many
return ACTION_INTERNAL_REQUESTS.equals(intent.getAction());
}
private void loadRequestsIntent(Intent currentIntent) {
if (isRequestsIntent(currentIntent)) {
Log.d(TAG, "Is requests intent.");
requestsIntent = currentIntent;
} else {
// Attempt to grab pending intent and send Otherwise initialize requestsIntent
Intent internalRunIntent = getRequestsIntent();
PendingIntent selfLaunchingIntent = PendingIntent.getService(this, 0, internalRunIntent, PendingIntent.FLAG_NO_CREATE);
if (selfLaunchingIntent == null) {
requestsIntent = internalRunIntent;
} else {
try {
Log.i(TAG, "Sending requests pending intent and waiting");
selfLaunchingIntent.send();
queueIntent(currentIntent, true);
pauseQueueUntilIntentReceived(internalRunIntent, null);
} catch (CanceledException e) {
Log.e(TAG, "CANCELLED INTERNAL RUN INTENT");
requestsIntent = internalRunIntent;
}
}
}
}
private SharedPreferences getHistoryPrefs() {
if (historyPrefs == null) { // Load prefs off main thread
historyPrefs = getSharedPreferences("PROBE_" + getClass().getName(), MODE_PRIVATE);
}
return historyPrefs;
}
protected void onHandleIntent(Intent intent) {
String action = intent.getAction();
Log.d(TAG, getDisplayName() + ": " + action);
Log.d(TAG, "Component: " + intent.getComponent() == null ? "<none>" : intent.getComponent().getClassName());
getHistoryPrefs();
if (requestsIntent == null) {
loadRequestsIntent(intent);
if (requestsIntent == null) {
Log.d(TAG, "Did not successfully load requests Intent");
return;
}
}
updateRequests();
Log.d(TAG, "RunIntent " + (requestsIntent == null ? "<null>" : "exists"));
if (intent.getComponent().getClassName().equals(Probe.class.getName())) { // Internally queued, not available outside of probe class
if (ACTION_RUN.equals(action) || ACTION_STOP.equals(action) || ACTION_DISABLE.equals(action)) {
ProbeScheduler scheduler = getScheduler();
ArrayList<Intent> requests = requestsIntent.getParcelableArrayListExtra(INTERNAL_REQUESTS_KEY);
Log.d(TAG, "Requests:" + requests);
if (isAvailableOnDevice() && !ACTION_DISABLE.equals(action) && scheduler.shouldBeEnabled(this, requests)) {
if(ACTION_RUN.equals(action)) {
_run();
} else {
_stop();
}
} else {
_disable();
updateInternalRequestsPendingIntent() ;
}
Long nextScheduledTime = scheduler.scheduleNextRun(this, requests);
Log.d(TAG, "Next scheduled time: " + nextScheduledTime);
if (nextScheduledTime == null) {
getHistoryPrefs().edit().remove(NEXT_RUN_TIME_KEY).commit();
} else {
getHistoryPrefs().edit().putLong(NEXT_RUN_TIME_KEY, nextScheduledTime).commit();
}
} else if (ACTION_INTERNAL_CALLBACK_REGISTERED.equals(action)){
Intent callbackRegisteredIntent = intent.getParcelableExtra(INTERNAL_CALLBACK_INTENT);
PendingIntent callback = intent.getParcelableExtra(CALLBACK_KEY);
_callback_registered(callbackRegisteredIntent, callback);
}
} else if (ACTION_REQUEST.equals(action) || action == null) {
ArrayList<Bundle> test = Utils.getArrayList(intent.getExtras(), REQUESTS_KEY);
Log.d(TAG, "REQUEST: " + test);
boolean succesfullyQueued = queueRequest(intent);
if (succesfullyQueued) {
run();
}
} else if (ACTION_INTERNAL_REQUESTS.equals(action)) {
} else if (ACTION_RUN.equals(action)) { // External stop, queue up stop
run();
} else if (ACTION_STOP.equals(action)) { // External stop, queue up stop
stop();
} else if (ACTION_DISABLE.equals(action)) { // External disable, queue up disable
disable();
} else if(ACTION_SEND_DETAILS.equals(action)) { // DOESN'T NEED requests
PendingIntent callback = intent.getParcelableExtra(CALLBACK_KEY);
sendProbeDetails(callback);
} else if(ACTION_SEND_CONFIGURATION.equals(action)) {
//PendingIntent callback = intent.getParcelableExtra(CALLBACK_KEY);
//sendProbeConfiguration(callback);
} else if(ACTION_SEND_STATUS.equals(action)) { // DOESN'T NEED requests
PendingIntent callback = intent.getParcelableExtra(CALLBACK_KEY);
sendProbeStatus(callback);
} else {
onHandleCustomIntent(intent);
}
}
/**
* Probe subclasses can override this to receive custom intents that the base probe does not handle.
* For instance, data intents from other probes.
* @param intent
*/
protected void onHandleCustomIntent(Intent intent) {
}
private void updateRequests() {
updateRequests(false);
}
private static Map<PendingIntent,Intent> getCallbacksToRequests(ArrayList<Intent> requests) {
Map<PendingIntent,Intent> existingCallbacksToRequests = new HashMap<PendingIntent,Intent>();
for (Intent existingRequest : requests) {
PendingIntent callback = existingRequest.getParcelableExtra(CALLBACK_KEY);
existingCallbacksToRequests.put(callback, existingRequest);
}
return existingCallbacksToRequests;
}
/**
* Updates request list with items in queue, replacing duplicate pending intents for this probe.
* @param requests
*/
private void updateRequests(boolean removeRunOnce) {
assert requestsIntent != null;
boolean hasChanges = false;
ArrayList<Intent> requests = requestsIntent.getParcelableArrayListExtra(INTERNAL_REQUESTS_KEY);
if (requests == null) {
hasChanges = true;
requests = new ArrayList<Intent>();
}
// Remove run once requests
Parameter periodParam = Parameter.getAvailableParameter(getAvailableParameters(), Parameter.Builtin.PERIOD);
if (periodParam != null && removeRunOnce) {
for (Intent request : requests) {
ArrayList<Bundle> dataRequests = Utils.getArrayList(request.getExtras(), REQUESTS_KEY);
List<Bundle> runOnceDataRequests = new ArrayList<Bundle>();
for (Bundle dataRequest : dataRequests) {
long periodValue = Utils.getLong(dataRequest, Parameter.Builtin.PERIOD.name, (Long)periodParam.getValue());
if (periodValue == 0L) {
Log.d(TAG, "Removing run once dataRequest: " + dataRequest);
runOnceDataRequests.add(dataRequest);
}
}
dataRequests.removeAll(runOnceDataRequests);
if (dataRequests.isEmpty()) {
deadRequests.add(request);
} else {
request.putExtra(REQUESTS_KEY, dataRequests);
}
}
}
// Remove all requests that we aren't able to (or supposed to) send to anymore
if (!deadRequests.isEmpty()) {
hasChanges = true;
for (Intent deadRequest = deadRequests.poll(); deadRequest != null; deadRequest = deadRequests.poll()) {
Log.d(TAG, "Removing dead request: " + deadRequest);
requests.remove(deadRequest);
}
}
// Add any pending requests
if (!pendingRequests.isEmpty()) {
hasChanges = true;
Map<PendingIntent,Intent> existingCallbacksToRequests = new HashMap<PendingIntent,Intent>();
for (Intent existingRequest : requests) {
PendingIntent callback = existingRequest.getParcelableExtra(CALLBACK_KEY);
existingCallbacksToRequests.put(callback, existingRequest);
}
for (Intent request = pendingRequests.poll(); request != null; request = pendingRequests.poll()) {
PendingIntent callback = request.getParcelableExtra(CALLBACK_KEY);
if (packageHasRequiredPermissions(this, callback.getTargetPackage(), getRequiredPermissions())) {
existingCallbacksToRequests.containsKey(callback);
int existingRequestIndex = requests.indexOf(existingCallbacksToRequests.get(callback));
ArrayList<Bundle> dataRequests = Utils.getArrayList(request.getExtras(), REQUESTS_KEY);
Log.d(TAG, "Adding pending intent with data requests: " + dataRequests);
if (existingRequestIndex >= 0) {
if (dataRequests == null || dataRequests.isEmpty()) {
Log.d(TAG, "Adding pending intent, removing because empty or null");
requests.remove(existingRequestIndex);
} else {
requests.set(existingRequestIndex, request);
}
} else {
if (dataRequests != null && !dataRequests.isEmpty()) { // Only add requests with nonempty data requests
Log.d(TAG, "Adding new pending intent: " + request);
requests.add(request);
}
}
} else {
Log.w(TAG, "Package '" + callback.getTargetPackage() + "' does not have the required permissions to get data from this probe.");
}
}
}
if (hasChanges) {
requestsIntent.putExtra(INTERNAL_REQUESTS_KEY, requests);
updateInternalRequestsPendingIntent();
}
}
private void updateInternalRequestsPendingIntent() {
PendingIntent internalPendingIntent = PendingIntent.getService(this, 0, requestsIntent, PendingIntent.FLAG_UPDATE_CURRENT);
// Keep the pending intent valid, by having the alarm clock keep a reference to it
AlarmManager manager = (AlarmManager)getSystemService(ALARM_SERVICE);
manager.set(AlarmManager.RTC, System.currentTimeMillis() + FAR_IN_FUTURE_MILLIS, internalPendingIntent);
}
/**
* Returns true if request was successfully queued
* @param request
* @return
*/
private boolean queueRequest(Intent request) {
// Check for pending intent
PendingIntent callback = null;
try {
callback = request.getParcelableExtra(CALLBACK_KEY);
} catch (Exception e) {
Log.e(TAG, "Request sent invalid callback.");
}
if (callback == null) {
Log.e(TAG, "Request did not send callback.");
} else {
boolean succesfullyQueued = pendingRequests.offer(request);
if (succesfullyQueued) {
Log.i(TAG, "Queued request from package '" + callback.getTargetPackage() + "'");
return true;
} else {
Log.e(TAG, "Unable to queue request from package '" + callback.getTargetPackage() + "'");
}
}
return false;
}
private void setHistory(Long previousDataSentTime, Long previousRunTime, Bundle previousRunParams, Long nextRunTime) {
SharedPreferences.Editor editor = getHistoryPrefs().edit();
editor.clear();
if (previousDataSentTime != null) editor.putLong(MOST_RECENT_DATA_KEY, previousDataSentTime);
if (previousRunTime != null) editor.putLong(MOST_RECENT_RUN_KEY, previousRunTime);
if (nextRunTime != null) editor.putLong(NEXT_RUN_TIME_KEY, nextRunTime);
try {
if (previousRunParams != null) Utils.putInPrefs(editor, MOST_RECENT_PARAMS_KEY, previousRunParams);
} catch (UnstorableTypeException e) {
Log.e(TAG, e.getLocalizedMessage());
}
editor.commit();
}
/**
* Resets all of the run data information, and removes all requests
*/
public void reset() {
getHistoryPrefs().edit().clear().commit();
if (requestsIntent != null) {
requestsIntent.removeExtra(INTERNAL_REQUESTS_KEY);
}
PendingIntent selfLaunchingIntent = PendingIntent.getService(this, 0, getRequestsIntent(), PendingIntent.FLAG_NO_CREATE);
if (selfLaunchingIntent != null) {
selfLaunchingIntent.cancel();
}
}
/**
* @return a timestamp (in millis since epoch) of the most recent time this probe sent data
*/
public long getPreviousDataSentTime() {
return getHistoryPrefs().getLong(MOST_RECENT_DATA_KEY, 0L);
}
/**
* @return a timestamp (in millis since epoch) of the most recent time this probe was run
*/
public long getPreviousRunTime() {
return getHistoryPrefs().getLong(MOST_RECENT_RUN_KEY, 0L);
}
/**
* @return the bundle of params used to run the probe the most recent time it was run
*/
public Bundle getPreviousRunParams() {
return Utils.getBundleFromPrefs(getHistoryPrefs(), MOST_RECENT_PARAMS_KEY);
}
/**
* @return a timestamp (in millis since epoch) of the most recent time this probe was run
*/
public long getNextRunTime() {
return getHistoryPrefs().getLong(NEXT_RUN_TIME_KEY, 0L);
}
/**
* Return the required set of permissions needed to run this probe
* @return
*/
public abstract String[] getRequiredPermissions();
/**
* Return the required set of features needed to run this probe
* @return
*/
public abstract String[] getRequiredFeatures();
/**
* @return Bundle of key value pairs that represent default parameters
*/
public abstract Parameter[] getAvailableParameters();
/**
* @return true if the necessary hardware or features are available on this device to run this probe.
*/
public boolean isAvailableOnDevice() {
return true;
}
/**
* Safe method to run probe, which ensures that requests are preserved.
*/
protected void run() {
if (requestsIntent != null) { // Assume the app is already disabled if requestsIntent is null
Intent i = new Intent(ACTION_RUN, null, this, Probe.class);
queueIntent(i);
}
}
protected final void stop() {
Log.d(TAG, "Stop queued");
if (requestsIntent != null) { // Assume the app is already disabled if requestsIntent is null
Intent i = new Intent(ACTION_STOP, null, this, Probe.class);
queueIntent(i);
}
}
protected final void disable() {
Log.d(TAG, "Disable queued");
if (requestsIntent != null) { // Assume the app is already disabled if requestsIntent is null
Intent i = new Intent(ACTION_DISABLE, null, this, Probe.class);
queueIntent(i);
}
}
/**
* Enables probe. Can only be called from intent handler thread
*/
private void _enable() {
assert isIntentHandlerThread();
if (!enabled) {
Log.i(TAG, "Enabling probe: " + getClass().getName());
enabled = true;
running = false;
sendProbeStatus(null);
onEnable();
}
}
/**
* Runs probe. Can only be called from intent handler thread
*/
private void _run() {
assert isIntentHandlerThread();
if (!enabled) {
_enable();
}
ProbeScheduler scheduler = getScheduler();
ArrayList<Intent> requests = requestsIntent.getParcelableArrayListExtra(INTERNAL_REQUESTS_KEY);
Bundle parameters = scheduler.startRunningNow(this, requests);
if (parameters != null) {
Log.i(TAG, "Running probe: " + getClass().getName());
running = true;
if (lock == null) {
lock = Utils.getWakeLock(this);
}
sendProbeStatus(null);
long nowTimestamp = Utils.millisToSeconds(System.currentTimeMillis());
Log.i(TAG, "Setting probe " + getClass().getName() + "'s last time to: " + String.valueOf(nowTimestamp));
getHistoryPrefs().edit().putLong(MOST_RECENT_RUN_KEY, nowTimestamp).commit();
onRun(parameters);
}
}
/**
* Stops probe. Can only be called from intent handler thread
*/
private void _stop() {
assert isIntentHandlerThread();
if (enabled && running) {
Log.i(TAG, "Stopping probe: " + getClass().getName());
onStop();
running = false; // TODO: possibly this should go before onStop, to keep with convention
sendProbeStatus(null);
// Remove one off requests
updateRequests(true);
if (lock != null && lock.isHeld()) {
lock.release();
lock = null;
}
}
}
/**
* Disables probe. Can only be called from intent handler thread
*/
private void _disable() {
assert isIntentHandlerThread();
if (enabled) {
Log.i(TAG, "Disabling probe: " + getClass().getName());
if (running) {
_stop();
}
enabled = false;
sendProbeStatus(null);
onDisable();
}
}
/**
* @return true if actively running, otherwise false
*/
public boolean isEnabled() {
return enabled;
}
/**
* @return true if actively running, otherwise false
*/
public boolean isRunning() {
return enabled ? running : false;
}
@Override
protected void onEndOfQueue() {
if (isEnabled() == false) {
stopSelf();
}
}
/**
* Start actively running the probe. Send data broadcast when done (or when appropriate) and stop.
*/
protected abstract void onEnable();
/**
* Start actively running the probe. Send data broadcast when done (or when appropriate) and stop.
* @param params
*/
protected abstract void onRun(Bundle params);
/**
* Stop actively running the probe. Any passive listeners should continue running.
*/
protected abstract void onStop();
/**
* Disable any passive listeners and tear down the service. Will be called when probe service is destroyed.
*/
protected abstract void onDisable();
/* TODO: may be a good way to make sure your configuration is set correctly
public void sendProbeConfiguration() {
}
*/
public void sendProbeDetails(PendingIntent callback) {
Details details = new Details(getClass().getName(), getDisplayName(), getRequiredPermissions(), getRequiredFeatures(), getAvailableParameters());
Intent detailsIntent = new Intent(ACTION_DETAILS);
detailsIntent.putExtras(details.getBundle());
callback(Utils.millisToSeconds(System.currentTimeMillis()), detailsIntent, callback);
}
public void sendProbeStatus(PendingIntent callback) {
Status status = new Status(getClass().getName(), enabled, isRunning(), getNextRunTime(), getPreviousRunTime());
Intent statusValuesIntent = new Intent(ACTION_STATUS);
statusValuesIntent.putExtras(status.getBundle());
callback(Utils.millisToSeconds(System.currentTimeMillis()),statusValuesIntent, callback);
}
/**
* Sends a DATA broadcast of the current data from the probe, if it is available.
*/
public abstract void sendProbeData();
// TODO: not sure we want this anymore
// This requires probes keep caches of last data sent.
// May not be useful unless we have a "send last data" feature.
/**
* Sends a DATA broadcast for the probe, and records the time.
*/
protected void sendProbeData(long epochTimestamp, Bundle data) {
// Should always be loaded when enabled
Intent dataIntent = new Intent(ACTION_DATA);
dataIntent.putExtras(data);
callback(epochTimestamp, dataIntent, null);
}
/**
* Send some values to each requesting pending intent
* @param valuesIntent
*/
protected void callback(long epochTimestamp, Intent valuesIntent, PendingIntent callback) {
assert requestsIntent != null;
valuesIntent.putExtra(PROBE, getClass().getName());
valuesIntent.putExtra(TIMESTAMP, epochTimestamp);
Log.d(TAG, "Queing probe " + valuesIntent.getAction() + " callback at " + epochTimestamp);
if (isIntentHandlerThread()) {
_callback_registered(valuesIntent, callback);
} else {
// Run on message queue to avoid concurrent modification of requests
Intent callbackRegisteredIntent = new Intent(ACTION_INTERNAL_CALLBACK_REGISTERED, null, this, Probe.class);
callbackRegisteredIntent.putExtra(INTERNAL_CALLBACK_INTENT, valuesIntent);
callbackRegisteredIntent.putExtra(CALLBACK_KEY, callback);
queueIntent(callbackRegisteredIntent);
}
}
private static boolean isDataRequestAcceptingPassiveData(Bundle dataRequest, Parameter[] availableParameters) {
boolean acceptOpportunisticData = true;
Parameter opportunisticParameter = Parameter.getAvailableParameter(availableParameters, Parameter.Builtin.OPPORTUNISTIC);
if (opportunisticParameter != null) {
acceptOpportunisticData = dataRequest.getBoolean(opportunisticParameter.getName(), (Boolean)opportunisticParameter.getValue());
}
return acceptOpportunisticData;
}
private boolean isDataRequestSatisfied(Bundle dataRequest, Parameter[] availableParameters, long dataTime, long lastDataSentTime) {
Parameter periodParameter = Parameter.getAvailableParameter(availableParameters, Parameter.Builtin.PERIOD);
if (periodParameter == null) {
return true;
} else {
long period = Utils.getLong(dataRequest, periodParameter.getName(), (Long)periodParameter.getValue());
return dataTime >= (lastDataSentTime + period);
}
}
/**
* Send some values to each requesting pending intent
* @param valuesIntent
*/
protected void _callback_registered(Intent valuesIntent, PendingIntent callback) {
long epochTimestamp = valuesIntent.getLongExtra(TIMESTAMP, 0L);
Set<PendingIntent> callbacks = new HashSet<PendingIntent>();
if (callback != null) {
callbacks.add(callback);
}
ArrayList<Intent> requests = null;
if (requestsIntent != null) {
requests = requestsIntent.getParcelableArrayListExtra(INTERNAL_REQUESTS_KEY);
}
if (ACTION_DATA.equals(valuesIntent.getAction())) {
// Send to all requesters
if (requests != null && !requests.isEmpty()) {
JSONObject dataSentTimes = null;
try {
dataSentTimes = new JSONObject(getHistoryPrefs().getString(MOST_RECENT_DATA_BY_REQUEST_KEY, "{}"));
} catch (JSONException e) {
Log.e(TAG, "Unable to parse data sent history.");
dataSentTimes = new JSONObject();
}
Parameter[] availableParameters = getAvailableParameters();
for (Intent request : requests) {
PendingIntent requesterCallback = request.getParcelableExtra(CALLBACK_KEY);
ArrayList<Bundle> dataRequests = Utils.getArrayList(request.getExtras(), REQUESTS_KEY);
for (Bundle dataRequest : dataRequests) {
String requestKey = normalizedStringRepresentation(dataRequest, getAvailableParameters());
long lastDataSentTime = dataSentTimes.optLong(requestKey);
// TODO: If data request accepts passive data, but also needs data enforced on a schedule
// we may need to make logic more complicated
if (isDataRequestSatisfied(dataRequest, availableParameters, epochTimestamp, lastDataSentTime)
|| isDataRequestAcceptingPassiveData(dataRequest, availableParameters)) {
callbacks.add(requesterCallback);
try {
dataSentTimes.put(requestKey, epochTimestamp);
} catch (JSONException e) {
Log.e(TAG, "Unable to store data sent time in .");
}
}
}
}
// Save the data sent times
Log.d(TAG, "Saving sent time as: " + String.valueOf(epochTimestamp));
getHistoryPrefs().edit()
.putString(MOST_RECENT_DATA_BY_REQUEST_KEY, dataSentTimes.toString())
.putLong(MOST_RECENT_DATA_KEY, epochTimestamp)
.commit();
}
} else {
// Add all
for (Intent request : requests) {
PendingIntent requesterCallback = request.getParcelableExtra(CALLBACK_KEY);
callbacks.add(requesterCallback);
}
}
Log.d(TAG, "Sent probe data at " + epochTimestamp);
for (PendingIntent requesterCallback : callbacks) {
try {
requesterCallback.send(this, 0, valuesIntent);
} catch (CanceledException e) {
Log.w(TAG, "Unable to send to canceled pending intent at " + epochTimestamp);
Map<PendingIntent,Intent> callbacksToRequests = getCallbacksToRequests(requests);
Intent request = callbacksToRequests.get(requesterCallback);
if (request != null) {
deadRequests.add(request);
}
}
}
updateRequests();
}
protected String getDisplayName() {
String className = getClass().getName().replace(getClass().getPackage().getName() + ".", "");
return className.replaceAll("(\\p{Ll})(\\p{Lu})","$1 $2"); // Insert spaces
}
private static List<PackageInfo> apps;
private static long appsLastLoadTime = 0L;
private static final long APPS_CACHE_TIME = Utils.secondsToMillis(300); // 5 minutes
/**
* This function is obsolete, replaced with packageHasRequiredPermissions
* @param context
* @param packageName
* @return
*/
/*
private static PackageInfo getPackageInfo(Context context, String packageName) {
long now = System.currentTimeMillis();
if (apps == null || now > (appsLastLoadTime + APPS_CACHE_TIME)) {
apps = context.getPackageManager().getInstalledPackages(PackageManager.GET_PERMISSIONS);
appsLastLoadTime = now;
}
for (PackageInfo info : apps) {
if (info.packageName.equals(packageName)) {
return info;
}
}
return null;
}*/
/**
* This function causes FAILED_BINDER_TRANSACTION because it requests all permissions for all apps installed. Fixed version below.
* @param context
* @param packageName
* @param requiredPermissions
* @return
*/
/*private static boolean packageHasRequiredPermissions(Context context, String packageName, String[] requiredPermissions) {
Log.v(Utils.TAG, "Getting package info for '" + packageName + "'");
PackageInfo info = getPackageInfo(context, packageName);
if (info == null) {
Log.w(Utils.TAG, "Package '" + packageName + "' is not installed.");
return false;
}
Set<String> packagePermissions = new HashSet<String>(Arrays.asList(info.requestedPermissions));
Log.v(Utils.TAG, "Package permissions for '" + packageName + "': " + Utils.join(packagePermissions, ", "));
for (String permission : nonNullStrings(requiredPermissions)) {
if (!packagePermissions.contains(permission)) {
Log.w(Utils.TAG, "Package '" + packageName + "' does not have the required permission '" + permission + "' to run this probe.");
return false;
}
}
return true;
}*/
/**
* This is the updated version of the function. Instead of polling the full list of all apps and all their permissions,
* it just compares the list of permission of a given package with the list of required permissions
* @param context
* @param packageName
* @param requiredPermissions
* @return <tt>false</tt> if one or more of the permissions are missing, <tt>true</tt> if all permissions are granted
*/
private static boolean packageHasRequiredPermissions(Context context, String packageName, String[] requiredPermissions) {
Log.v(Utils.TAG, "PIOTR'S VERSION. Getting package info for '" + packageName + "'");
for (String permission : nonNullStrings(requiredPermissions)) {
if (context.getPackageManager().checkPermission(permission, packageName) == PackageManager.PERMISSION_DENIED) {
Log.w(Utils.TAG, "Package '" + packageName + "' does not have the required permission '" + permission + "' to run this probe.");
return false;
}
}
return true;
}
/**
* Returns the scheduler object used to schedule this probe.
* Probes should override this to change how their probe gets scheduled.
* @return
*/
protected ProbeScheduler getScheduler() {
return new DefaultProbeScheduler();
}
public final static class Status {
private Bundle bundle;
public Status(String name,
boolean enabled, boolean running,
long nextRun, long previousRun) {
this.bundle = new Bundle();
bundle.putBoolean("ENABLED", enabled);
bundle.putBoolean("RUNNING", running);
bundle.putLong("NEXT_RUN", nextRun);
bundle.putLong("PREVIOUS_RUN", previousRun);
bundle.putString(Probe.PROBE, name);
}
public Status(Bundle bundle) {
this.bundle = new Bundle(bundle);
}
public String getProbe() {
return bundle.getString(Probe.PROBE);
}
public boolean isEnabled() {
return bundle.getBoolean("ENABLED");
}
public boolean isRunning() {
return bundle.getBoolean("RUNNING");
}
public long getNextRun() {
return bundle.getLong("NEXT_RUN");
}
public long getPreviousRun() {
return bundle.getLong("PREVIOUS_RUN");
}
public Bundle getBundle() {
return bundle;
}
@Override
public boolean equals(Object o) {
return o != null
&& o instanceof Status
&& getProbe().equals(((Status)o).getProbe());
}
@Override
public int hashCode() {
return getProbe().hashCode();
}
}
public final static class Details {
private Bundle bundle;
public Details(String name, String displayName,
String[] requiredPermissions,
String[] requiredFeatures,
Parameter[] parameters) {
this.bundle = new Bundle();
bundle.putString(PROBE, name);
bundle.putString("DISPLAY_NAME", displayName);
bundle.putStringArray("REQUIRED_PERMISSIONS", requiredPermissions == null ? new String[]{} : requiredPermissions);
bundle.putStringArray("REQUIRED_FEATURES", requiredFeatures == null ? new String[]{} : requiredFeatures);
ArrayList<Bundle> paramBundles = new ArrayList<Bundle>();
if (parameters != null) {
for (Parameter param : parameters) {
paramBundles.add(param.getBundle());
}
}
bundle.putParcelableArrayList("PARAMETERS", paramBundles);
}
public Details(Bundle bundle) {
this.bundle = bundle;
}
public String getName() {
return bundle.getString(PROBE);
}
public String getDisplayName() {
return bundle.getString("DISPLAY_NAME");
}
public String[] getRequiredPermissions() {
return bundle.getStringArray("REQUIRED_PERMISSIONS");
}
public String[] getRequiredFeatures() {
return bundle.getStringArray("REQUIRED_FEATURES");
}
public Parameter getParameter(final String name) {
for(Parameter parameter : getParameters()) {
if (parameter.getName().equalsIgnoreCase(name)) {
return parameter;
}
}
return null;
}
public Parameter[] getParameters() {
ArrayList<Bundle> paramBundles = bundle.getParcelableArrayList("PARAMETERS");
List<Parameter> paramList = new ArrayList<Parameter>();
for (Bundle paramBundle : paramBundles) {
paramList.add(new Parameter(paramBundle));
}
Parameter[] parameters = new Parameter[paramBundles.size()];
paramList.toArray(parameters);
return parameters;
}
public Bundle getBundle() {
return bundle;
}
@Override
public boolean equals(Object o) {
return o != null
&& o instanceof Details
&& getName().equals(((Details)o).getName());
}
@Override
public int hashCode() {
return getName().hashCode();
}
}
/**
* Represents a parameter that can be passed to a probe
* @author alangardner
*
*/
public final static class Parameter {
/**
* The built-in parameters that the Funf system knows how to handle
* @author alangardner
*
*/
public enum Builtin {
OPPORTUNISTIC("OPPORTUNISTIC", "Opportunistic", "Whether the requester wants data they did not specifically request."),
DURATION("DURATION", "Duration", "Length of time probe will run for (seconds)"),
START("START_DATE", "Start Timestamp", "Date after which probe is allowed to run (seconds since epoch)"),
END("END_DATE", "End Timestamp", "Date before which probe is allowed to run (seconds since epoch)"),
PERIOD("PERIOD", "Period", "Length of time between probe runs (seconds)");
public final String name, displayName, description;
private Builtin(String name, String displayName, String description) {
this.name = name;
this.displayName = displayName;
this.description = description;
}
}
public static final String NAME_KEY = "NAME";
public static final String DEFAULT_VALUE_KEY = "DEFAULT_VALUE";
public static final String DISPLAY_NAME_KEY = "DISPLAY_NAME";
public static final String DESCRIPTION_KEY = "DESCRIPTION";
private final Bundle paramBundle;
/**
* Custom parameter constructor
* @param name
* @param value
* @param displayName
* @param description
*/
public Parameter(final String name, final Object value, final String displayName, final String description) {
paramBundle = new Bundle();
paramBundle.putString(NAME_KEY, name);
paramBundle.putString(DISPLAY_NAME_KEY, displayName);
paramBundle.putString(DESCRIPTION_KEY, description);
Utils.putInBundle(paramBundle, DEFAULT_VALUE_KEY, value);
}
public Parameter(final Bundle paramBundle) {
// TODO: we might want to ensure that the bundle has the appropriate keys
this.paramBundle = paramBundle;
}
/**
* System parameter constructor, to be handled by system
* @param paramType
* @param defaultValue
* @param supportedByProbe
*/
public Parameter(final Parameter.Builtin paramType, final Object defaultValue) {
this(paramType.name, defaultValue, paramType.displayName, paramType.description);
}
public String getName() {
return paramBundle.getString(NAME_KEY);
}
public Object getValue() {
return paramBundle.get(DEFAULT_VALUE_KEY);
}
public String getDisplayName() {
return paramBundle.getString(DISPLAY_NAME_KEY);
}
public String getDescription() {
return paramBundle.getString(DESCRIPTION_KEY);
}
public Bundle getBundle() {
return paramBundle;
}
public static Parameter getAvailableParameter(Parameter[] availableParameters, Builtin systemParam) {
for (Parameter p : availableParameters) {
if(systemParam.name.equals(p.getName())) {
return p;
}
}
return null;
}
}
public static String normalizedStringRepresentation(Bundle dataRequest, Parameter[] availableParameters) {
// We use JSONObject to ensure special characters are properly and consistently escaped during serialization
// But key order is not guaranteed, so we make many of them
List<String> representations = new ArrayList<String>();
for (Parameter parameter : availableParameters) {
String name = parameter.getName();
Object value = dataRequest.get(name);
if (value != null) {
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put(name, value);
representations.add(jsonObject.toString());
} catch (JSONException e) {
Log.e(Utils.TAG, "Unable to serialize parameter name '" + name + "' with value '" + value + "'");
}
}
}
Collections.sort(representations);
return "[" + Utils.join(representations, ",") + "]";
}
/**
* Binder interface to the probe
*/
public class LocalBinder extends Binder {
Probe getService() {
return Probe.this;
}
}
private final IBinder mBinder = new LocalBinder();
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}