Back to project page SpunkyCharts.
The source code is released under:
GNU General Public License
If you think the Android project SpunkyCharts listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.
package com.jogden.spunkycharts.data; /* // w w w .j ava2 s. c om Copyright (C) 2014 Jonathon Ogden < jeog.dev@gmail.com > This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses. */ import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.UUID; import com.jogden.spunkycharts.ApplicationPreferences; import com.jogden.spunkycharts.MainApplication; import com.jogden.spunkycharts.data.DataContentService.DataConsumerInterface.SQLType; import com.jogden.spunkycharts.misc.Pair; import android.app.Activity; import android.app.LoaderManager; import android.app.Service; import android.content.AsyncTaskLoader; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.Loader; import android.database.Cursor; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.os.AsyncTask; import android.os.Binder; import android.os.IBinder; import android.text.format.Time; import android.util.Log; /** This will be the back-end service that accepts user * defined, generic client and consumer interfaces to * handle data-retrieval from the servers in both bulk * form and streaming. * </p> * The DataClientInterface will be they layer of code * between the data service and the external data * supplier(server) that can be added by the user * to provide their own data source. * </p> * The DataClientConsumer will be implemented by your * ChartFragmentAdapter so the data service can callback * to it when data is received. * </p> * A basic outline of how the service works: * <ol> * <li> InitActivity checks connection...</li> * <li> if O.K calls MainApplication.connectToDataService which: </li> * <ul> * <li> 'binds' the service to the application context, </li> * <li> attempts to start the service </li> * <li> saves a global reference before calling DataContentService.connect() </li> * </ul> * <li> DataContentService.connect() will attempt to: </li> * <ul> * <li> call down to your custom DataClientInterface's .connect() method, </li> * <li> if that returns true it will start the MainUpdateLoop which will: </li> * <ul> * <li> asynchronously loop through all the DataClientConsumers(your adapters), </li> * <li> calling their updateReady methods, </li> * <li> if that returns true it calls back to their update(...) method </li> * </ul> * <li> (note: updateReady() probably should return false until step #6) </li> * </ul> * <b> AT THIS POINT DataContentSerivce IS DONE INITIALIZING </b> * </br></br> *<li> YourChartFragment calls DataContentService.addChannel(...) which * first calls back to YourChartFragmentAdapter.getColumns() method. This method * should simply return a List of Pairs used for setting up the SQLlite table your data * will be cached in. Each pair should have a) a unique string of the column name and * b) the appropriate DataConsumerInterface.SQLType enum value representing the * type of data you will add(i.e. int, float, string.) * </p> * After it set's up the table it will call back to YourChartFragmentAdapter.bridge() * method passing the client interface and an InsertCallback. You can call * InsertCallback.clear(..) to drop the old data and start fresh. Ideally you'll use the * client's get methods, update the internal chart fragment state, and then use one * of InsertCallback's insert methods to insert the data into the database table. The idea * is to provide a large amount of flexibility in how you implement data retrieval, * storage, manipulation et al. while still providing enough abstraction and * encapsulation to avoid utter confusion and run-time disasters, respectively. * </li> * <li> YourChartFragment calls DataContentService.load(...) * which, when done asynchronously loading a cursor from the database, * calls back to YourChartFragmentAdapter.swapCursor(...) . Now when * you need to refresh your chart you can load from the database what you * need rather than having to re-download it from the server.</li> * <li> Check your cursor is valid and has the data you put into the database; if so * have updateReady() return true as long as you want your update() * function to be called continually by DataContentService. </li> * </ol> */ public class DataContentService extends Service { public class OurBinder extends Binder{ public DataContentService getService(){ return DataContentService.this; } } @SuppressWarnings("serial") static public class DataContentServiceException extends RuntimeException { public DataContentServiceException(String msg){ super(msg); } public DataContentServiceException(Exception e){ super(e); } } @SuppressWarnings("serial") static public class DataConnectionException extends DataContentServiceException { public DataConnectionException(String msg){ super(msg); } } @SuppressWarnings("serial") static public class DataRetrievalException extends DataContentServiceException { public DataRetrievalException(String msg) { super(msg); } } /** To be implemented by your data back-end */ public static interface DataClientInterface{ public final int TIME_GRANULARITY = 1; public <T> Pair<T[],Time> getBulk( String symbol, Time start, int type ); public <T> Pair<T,Time> getLast( String symbol, int type ); public boolean connect(); public boolean disconnect(); @SuppressWarnings("serial") final static public class DataClientException extends DataContentServiceException{ public DataClientException(String msg) { super(msg); } public DataClientException(Exception e) { super(e); } } } /** To be implemented by your fragment adapter */ public static interface DataConsumerInterface{ public final String primaryKey = "TIME_COLUMN"; public enum SQLType{ StringNotNull("string not null", String.class), IntNotNull("integer not null", Integer.class), FloatNotNull("float not null", Float.class); public String fullString; public Class<?> type; private SQLType(String str, Class<?> t){ fullString = str; type =t; } } public interface InsertCallback{ public void insertRow( ContentValues vals ,String symbol, DataConsumerInterface consumer ); public void insertRows( List<ContentValues> valsList, String symbol, DataConsumerInterface consumer ); public void clear( String symbol, DataConsumerInterface consumer ); } public void update( DataClientInterface dataClient, InsertCallback iCallback); public boolean updateReady(); public Cursor swapCursor(Cursor cursor); public void bridge( DataClientInterface dataClient, InsertCallback iCallback); public List<Pair<String,SQLType>> getColumns(); } static private int callbacksB4Trim = ApplicationPreferences.getCallbacksB4Trim(); static private long cacheSizeMS = /*debug 1200000; */ ApplicationPreferences.getDataCacheDays() * 86400000; private final IBinder _myBinder = new OurBinder(); static private boolean connected = false; /** Database notes: * </p> * Ideally myDatabase would not use ROWID, * but that requires SQLite v 3.8.2 or higher * </p> * switched ROWID to TIME_COLUMN integer primary key * (removing autoincrement) managing time in long ms, * to deal with bulk removes and improve performance * </p> * Currently we are checking max-size on our end (after * callbacksB4Trim rows are added and we trim all rows * whose TIME_COLUMN val < last row TIME_COLUMN val - * cacheSizeMS ). This means at some point ROWID vals will * be > MAX_LONG and new ROWIDs will be re-assigned from * deleted rows which means: WE CANT ASSUME ROWID VALS * ARE ORDERED OR CONTIGUOUS; if that type of behavior is * required use integer TIME_COLUMN instead. * */ static private SQLiteDatabase myDatabase = null; static private MainUpdateLoop myUpdateLoop = null; static private DataClientInterface myDataClient = null; static private final Map<String,Pair<List<DataConsumerInterface>,Integer>> channels = new HashMap<String, Pair<List<DataConsumerInterface>,Integer>>(); /*|---------> begin PUBLIC interface <--------|*/ static public void setCallbacksB4Trim(int callbacks) { callbacksB4Trim = callbacks; } static public void setDataCacheDays(int days) { cacheSizeMS = days * 86400000; } static public void setDataClientInterface(DataClientInterface client) { myDataClient = client; } static public final void connect() { if(connected) return; if( !myDataClient.connect()) throw new DataConnectionException( "myDataClient could not initialize connection" ); else{ connected = true; myUpdateLoop = new MainUpdateLoop(); myUpdateLoop.start(); } } static public final void disconnect() { connected = false; myUpdateLoop.interrupt(); myUpdateLoop = null; removeTables(); myDataClient.disconnect(); } static public final void addChannel( String symbol, DataConsumerInterface consumer ){ if(!connected) throw new DataConnectionException( "DataContentService is not connected" ); if(symbol.isEmpty()) /* won't need this when frags check symbols */ throw new DataContentServiceException( "symbol string can not be empty" ); synchronized(channels){ String tName = genTableName(symbol, consumer); Pair<List<DataConsumerInterface>,Integer> pLstDci = channels.get(tName); if( pLstDci != null ){ if( !pLstDci.first.contains(consumer) ) pLstDci.first.add(consumer); return; } List<DataConsumerInterface> list = new LinkedList<DataConsumerInterface>(); list.add(consumer); Pair<List<DataConsumerInterface>,Integer> chnlPair = new Pair<List<DataConsumerInterface>,Integer>(list,0); channels.put(tName,chnlPair); createTable(tName,consumer); consumer.bridge( myDataClient, myInsertCallbacks ); } } static public final void removeChannel( String symbol, DataConsumerInterface consumer ){ synchronized(channels){ String tName = genTableName(symbol,consumer); List<DataConsumerInterface> list = channels.get(tName).first; if(list != null){ if(list.contains(consumer)){ list.remove(consumer); if(list.size() == 0){ channels.remove(tName); myDatabase.execSQL( "DROP TABLE IF EXISTS " + tName ); } } } } } static public final void load( String symbol, final DataConsumerInterface consumer ){ String tName = genTableName(symbol,consumer); if(!channels.containsKey(tName)) return; if(!connected) throw new DataConnectionException( "DataContentService is not connected" ); try{ (new SQLiteRawAsyncCursorLoader( myDatabase, new SQLiteRawAsyncCursorLoader.LoaderCallback(){ public void onLoadFinished(Cursor cur){ if(cur == null) throw new SQLException(); /* only swap on success (for now) */ consumer.swapCursor(cur); } }, tName, null, null, null, null, null, null, null )).start(); }catch(RuntimeException e){ throw new DataRetrievalException( "failure querying myDatabase in DataContentService" ); } } /*careful! this could be a lot of data */ static public final void dumpDatabaseTableToLogCat( String symbol, final DataConsumerInterface consumer ){ final String tableName = genTableName(symbol,consumer); try{ (new SQLiteRawAsyncCursorLoader( myDatabase, new SQLiteRawAsyncCursorLoader.LoaderCallback(){ public void onLoadFinished(Cursor cur){ if(cur == null) throw new SQLException(); int cols = cur.getColumnCount(); Time time = new Time(); while ( cur.moveToNext() ){ String dLine = ""; int i = 1; time.set(cur.getLong(0)); dLine += (time.format("%H:%M:%S") + " - "); for( ; i < cols - 1; ++i){ dLine += (cur.getString(i) + " - "); } dLine += cur.getString(i); Log.d("DataContentService-Dump-" +tableName, dLine ); } } }, tableName, null, null, null, null, null, null, null )).start(); }catch(RuntimeException e){ throw new DataRetrievalException( "failure querying myDatabase in DataContentService" ); } } /*|---------> end PUBLIC interface <--------|*/ /*|---------> begin PRIVATE methods <--------|*/ static private DataConsumerInterface.InsertCallback myInsertCallbacks = new DataConsumerInterface.InsertCallback() { private void checkAndTrim(String tName, long time, int incr){ long cutoff = time - cacheSizeMS; Integer cnt = channels.get(tName).second; if ((cnt=cnt+incr)> callbacksB4Trim){ myDatabase.delete( tName, DataConsumerInterface.primaryKey + " <= " + String.valueOf(cutoff), null ); cnt = 0; } channels.get(tName).second = cnt; } @Override public void clear( String symbol, DataConsumerInterface consumer ){ createTable( genTableName(symbol,consumer),consumer); } @Override public synchronized void insertRow( ContentValues vals, String symbol, DataConsumerInterface consumer ){ try{ String tName = genTableName(symbol,consumer); if( !( channels.get(tName).first.indexOf(consumer) == 0) ) return; myDatabase.beginTransaction(); myDatabase.insert( tName, null, vals ); checkAndTrim( tName, vals.getAsLong(DataConsumerInterface.primaryKey), 1 ); myDatabase.setTransactionSuccessful(); }catch(NullPointerException e){ e.printStackTrace(); return; }finally{ myDatabase.endTransaction(); } } @Override public synchronized void insertRows( List<ContentValues> valsList, String symbol, DataConsumerInterface consumer ) { try{ /* this only checks after ALL inserts; could theoretically overflow first */ String tName = genTableName(symbol,consumer); if( !( channels.get(tName).first.indexOf(consumer) == 0) ) return; myDatabase.beginTransaction(); for( ContentValues vals : valsList) myDatabase.insert( tName, null, vals ); int sz = valsList.size(); checkAndTrim( tName, valsList.get(sz-1).getAsLong(DataConsumerInterface.primaryKey), sz ); myDatabase.setTransactionSuccessful(); }catch(NullPointerException e){ e.printStackTrace(); return; }finally{ myDatabase.endTransaction(); } } }; static private String genTableName( String symbol, DataConsumerInterface consumer ){ int cHash = consumer.getClass().hashCode(); return symbol + String.valueOf(Math.abs(cHash)) + ((cHash < 0 ) ? "n" : "p"); /* can't have '-' sign */ } static private void removeTables() { synchronized(channels){ for( String s : channels.keySet() ) myDatabase.execSQL("DROP TABLE IF EXISTS " + s); } } static private void createTable( String tName, DataConsumerInterface consumer ){ myDatabase.execSQL("DROP TABLE IF EXISTS " + tName); List<Pair<String,SQLType>> colData = consumer.getColumns(); String createStr = "create table " + tName + " ( " + DataConsumerInterface.primaryKey + " integer not null primary key, "; for( Pair<String,SQLType> i : colData) createStr += (i.first + " " + i.second.fullString + ", "); createStr = createStr.substring(0, createStr.length() - 2); createStr+= ");"; myDatabase.execSQL(createStr); } /*|---------> end PRIVATE methods <--------|*/ /*|---------> begin LIFE-CYCLE handlers <--------|*/ @Override public void onCreate() { super.onCreate(); if(myDataClient != null){ /* create a memory mapped database */ myDatabase = SQLiteDatabase.create( null ); } else throw new IllegalStateException( "DataContentService.myDataClient can not be null" ); } @Override public int onStartCommand(Intent intent, int flags, int startId) { super.onStartCommand(intent, flags, startId); return START_STICKY; } @Override public IBinder onBind(Intent intent) { return _myBinder; } @Override public void onDestroy() { myDatabase.close(); super.onDestroy(); } /*|---------> end LIFE-CYCLE handlers <--------|*/ /*|---------> begin NESTED OBJECT DEFS <--------|*/ /* how we signal the consumers to update */ static private class MainUpdateLoop extends Thread { public void run(){ try { while(true){ synchronized(channels){ for(String key : channels.keySet() ) for( DataConsumerInterface dci : channels.get(key).first ) if( dci.updateReady() ) dci.update( myDataClient, myInsertCallbacks ); } Thread.sleep(5000); // DEBUG PARAM // } } catch (InterruptedException e) { } catch (RuntimeException e) { /* set breakpoint here to simplify debugging of runtime * exceptions that arise from deep within the update(...) stack. */ e.printStackTrace(); } } } /* how we asynchronously load a cursor from myDatabase; * no need to extend a loader, cleaner this way */ static private class SQLiteRawAsyncCursorLoader extends Thread { public interface LoaderCallback{ public void onLoadFinished(Cursor cur); } private SQLiteDatabase database; private final LoaderCallback callback; private String[] columns, selectionArgs; private String table, selection, groupBy, having, orderBy, limit; public SQLiteRawAsyncCursorLoader( SQLiteDatabase database, LoaderCallback callback, String tableName, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit ){ super(); this.database = database; this.callback = callback; this.columns = columns; this.selectionArgs = selectionArgs; this.table = tableName; this.selection = selection; this.groupBy = groupBy; this.having = having; this.orderBy = orderBy; this.limit = limit; } @Override public void run(){ if(database != null && table != null && table != ""){ Cursor cur = null; try{ cur = database.query( table, columns, selection, selectionArgs, groupBy, having, orderBy, limit ); }catch(RuntimeException e){ e.printStackTrace(); }finally{ callback.onLoadFinished(cur); } } } } /*|---------> end NESTED OBJECT DEFS <--------|*/ }