/** * * 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.builtin; import java.math.BigDecimal; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; import android.content.Context; import android.database.Cursor; import android.database.CursorIndexOutOfBoundsException; import android.util.Log; import com.google.gson.Gson; import com.google.gson.JsonObject; import edu.mit.media.funf.Schedule; import edu.mit.media.funf.Schedule.DefaultSchedule; import edu.mit.media.funf.probe.builtin.ContentProviderProbe.CursorCell.AnyCell; import edu.mit.media.funf.probe.builtin.ContentProviderProbe.CursorCell.BooleanCell; import edu.mit.media.funf.probe.builtin.ContentProviderProbe.CursorCell.DoubleCell; import edu.mit.media.funf.probe.builtin.ContentProviderProbe.CursorCell.HashedCell; import edu.mit.media.funf.probe.builtin.ContentProviderProbe.CursorCell.IntCell; import edu.mit.media.funf.probe.builtin.ContentProviderProbe.CursorCell.LongCell; import edu.mit.media.funf.probe.builtin.ContentProviderProbe.CursorCell.StringCell; import edu.mit.media.funf.security.HashUtil; import edu.mit.media.funf.util.LogUtil; @Schedule.DefaultSchedule(interval=3600) public abstract class ContentProviderProbe extends ImpulseProbe { private Gson gson; @Override protected void onStart() { super.onStart(); gson = getGson(); for (JsonObject data : parseCursorResults()) { if (data != null) { BigDecimal customTimestamp = getTimestamp(data); if (customTimestamp != null) { data.addProperty(TIMESTAMP, customTimestamp); } sendData(data); } } } private Iterable<JsonObject> parseCursorResults() { return new Iterable<JsonObject>() { @Override public Iterator<JsonObject> iterator() { return new ContentProviderIterator(); } }; } class ContentProviderIterator implements Iterator<JsonObject> { private final Cursor c; private final String[] projection; private final Map<String, CursorCell<?>> projectionMap; private boolean brandNew; // Next has not been called public ContentProviderIterator() { this.projectionMap = getProjectionMap(); this.projection = new String[projectionMap.size()]; projectionMap.keySet().toArray(projection); this.c = getCursor(projection); int count = c.getCount(); this.brandNew = true; Log.v(LogUtil.TAG, "cursor returned " + count +" result"); } @Override public boolean hasNext() { //Log.d(TAG, "Checking has next"); boolean hasNext = brandNew ? c.moveToFirst() : !(c.isClosed() || c.isLast() || c.isAfterLast()); if (!hasNext) c.close(); return hasNext; } @Override public JsonObject next() { if (brandNew) { c.moveToFirst(); brandNew = false; } else { c.moveToNext(); } JsonObject data = null; try { data = parseData(c, projection, projectionMap); } catch (CursorIndexOutOfBoundsException e) { throw new NoSuchElementException(); } finally { if (!hasNext() && !c.isClosed()) { //Log.d(TAG, "CLOSING cursor"); c.close(); } } return data; } @Override public void remove() { throw new UnsupportedOperationException(); } } protected abstract Map<String,CursorCell<?>> getProjectionMap(); protected abstract Cursor getCursor(String[] projection); protected BigDecimal getTimestamp(JsonObject data) { return null; } protected JsonObject parseData(Cursor cursor, String[] projection, Map<String,CursorCell<?>> projectionMap) { JsonObject data = new JsonObject(); for (String key : projection) { CursorCell<?> cursorCell = projectionMap.get(key); if (cursorCell != null) { Object value = cursorCell.getData(cursor, key); if (value != null) { data.add(key, gson.toJsonTree(value)); } } } return data; } // Convenience methods that can be used to cache and reuse CursorCell objects protected static BooleanCell booleanCell() { return new BooleanCell(); } protected static IntCell intCell() { return new IntCell(); } protected static LongCell longCell() { return new LongCell(); } protected static DoubleCell doubleCell() { return new DoubleCell(); } protected static StringCell stringCell() { return new StringCell(); } protected static AnyCell anyCell() { return new AnyCell(); } protected CursorCell<String> sensitiveStringCell() { return new SensitiveCell(stringCell()); // TODO: do standard normalizing of string (i.e. remove everything bug word chars, and lower case) } protected class SensitiveCell extends CursorCell<String> { // TODO: Possible return a json object, instead of a string private CursorCell<String> upstreamCell = stringCell(); public SensitiveCell(CursorCell<String> upstreamCell) { this.upstreamCell = upstreamCell; } @Override public String getData(Cursor cursor, int columnIndex) { return sensitiveData(upstreamCell.getData(cursor, columnIndex)); } } protected static CursorCell<String> hashedStringCell(Context context) { return new HashedCell(context, stringCell()); } public static abstract class CursorCell<T> { public abstract T getData(Cursor cursor, int columnIndex); public T getData(Cursor cursor, String columnName) { int index = cursor.getColumnIndex(columnName); if (index < 0) { return null; // Different devices have different columns available } return getData(cursor, cursor.getColumnIndex(columnName)); } public static class StringCell extends CursorCell<String> { public String getData(Cursor cursor, int columnIndex) { return cursor.getString(columnIndex); } } public static class BooleanCell extends CursorCell<Boolean> { public Boolean getData(Cursor cursor, int columnIndex) { return cursor.getInt(columnIndex) != 0; } } public static class ShortCell extends CursorCell<Short> { public Short getData(Cursor cursor, int columnIndex) { return cursor.getShort(columnIndex); } } public static class IntCell extends CursorCell<Integer> { public Integer getData(Cursor cursor, int columnIndex) { return cursor.getInt(columnIndex); } } public static class LongCell extends CursorCell<Long> { public Long getData(Cursor cursor, int columnIndex) { return cursor.getLong(columnIndex); } } public static class FloatCell extends CursorCell<Float> { public Float getData(Cursor cursor, int columnIndex) { return cursor.getFloat(columnIndex); } } public static class DoubleCell extends CursorCell<Double> { public Double getData(Cursor cursor, int columnIndex) { return cursor.getDouble(columnIndex); } } /** * Always return null. Used to block data from being returned. */ public static class NullCell extends CursorCell<Object> { @Override public Object getData(Cursor cursor, int columnIndex) { return null; } } /** * Try every type until success */ public static class AnyCell extends CursorCell<Object> { @Override public Object getData(Cursor cursor, int columnIndex) { if (cursor.isNull(columnIndex)) { return null; } try { return cursor.getShort(columnIndex); } catch (Exception e) {} try { return cursor.getInt(columnIndex); } catch (Exception e) {} try { return cursor.getLong(columnIndex); } catch (Exception e) {} try { return cursor.getFloat(columnIndex); } catch (Exception e) {} try { return cursor.getDouble(columnIndex); } catch (Exception e) {} try { return cursor.getString(columnIndex); } catch (Exception e) {} // Not returning blobs to contain size of bundles and prevent FAILED BINDER TRANSACTION //try { return cursor.getBlob(columnIndex); } catch (Exception e) {} return null; } } public static class PhoneNumberCell extends StringCell { @Override public String getData(Cursor cursor, int columnIndex) { return HashUtil.formatPhoneNumber(super.getData(cursor, columnIndex)); } } /** * TODO: redo this to pass in the hash strategy * */ public static class HashedCell extends CursorCell<String> { private final CursorCell<?> upstreamCell; private final Context context; public HashedCell(Context context, CursorCell<?> upstreamCell) { this.upstreamCell = upstreamCell; this.context = context; } public String getData(Cursor cursor, int columnIndex) { Object value = upstreamCell.getData(cursor, columnIndex); return (value == null) ? "" : HashUtil.hashString(context, String.valueOf(value)); } } } }