Android Open Source - SpunkyCharts Data Content Service






From Project

Back to project page SpunkyCharts.

License

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.

Java Source Code

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

}




Java Source Code List

com.jogden.spunkycharts.ApplicationPreferences.java
com.jogden.spunkycharts.BaseChartFragmentA.java
com.jogden.spunkycharts.ChartPanelSurfaceView.java
com.jogden.spunkycharts.DockingPanelActivity.java
com.jogden.spunkycharts.GlobalChartPreferences.java
com.jogden.spunkycharts.InitActivity.java
com.jogden.spunkycharts.MainApplication.java
com.jogden.spunkycharts.OpeningView.java
com.jogden.spunkycharts.animations.BaseAnimationA.java
com.jogden.spunkycharts.animations.BaseEntExAnimationA.java
com.jogden.spunkycharts.animations.BaseSelectAnimationA.java
com.jogden.spunkycharts.animations.HorizontalBulgeAnimation.java
com.jogden.spunkycharts.animations.HorizontalShakeAnimation.java
com.jogden.spunkycharts.animations.VerticalBulgeAnimation.java
com.jogden.spunkycharts.animations.VerticalShakeAnimation.java
com.jogden.spunkycharts.animations.WiggleAnimation.java
com.jogden.spunkycharts.data.DataClientLocalDebug.java
com.jogden.spunkycharts.data.DataContentService.java
com.jogden.spunkycharts.misc.BorderOverlay.java
com.jogden.spunkycharts.misc.ColorPaletteDialog.java
com.jogden.spunkycharts.misc.HideHorizontalLeftOverflowWrapper.java
com.jogden.spunkycharts.misc.OHLC.java
com.jogden.spunkycharts.misc.Pair.java
com.jogden.spunkycharts.misc.TextInput.java
com.jogden.spunkycharts.misc.Triple.java
com.jogden.spunkycharts.pricebyvolumechart.PriceByVolumeChartFragmentAdapter.java
com.jogden.spunkycharts.pricebyvolumechart.PriceByVolumeChartFragment.java
com.jogden.spunkycharts.pricebyvolumechart.PriceByVolumeChartPanel.java
com.jogden.spunkycharts.pricebyvolumechart.PriceByVolumeChartPreferences.java
com.jogden.spunkycharts.pricebyvolumechart.draw.DrawSemanticsA.java
com.jogden.spunkycharts.pricebyvolumechart.draw.DrawSemantics_SILO.java
com.jogden.spunkycharts.traditionalchart.InnerXAxis.java
com.jogden.spunkycharts.traditionalchart.TraditionalChartFragmentAdapter.java
com.jogden.spunkycharts.traditionalchart.TraditionalChartFragment.java
com.jogden.spunkycharts.traditionalchart.TraditionalChartPanel.java
com.jogden.spunkycharts.traditionalchart.TraditionalChartPreferences.java
com.jogden.spunkycharts.traditionalchart.XAxisTimeLabel.java
com.jogden.spunkycharts.traditionalchart.YAxisPriceLabel.java
com.jogden.spunkycharts.traditionalchart.draw.DrawSemanticsA_C.java
com.jogden.spunkycharts.traditionalchart.draw.DrawSemanticsA.java
com.jogden.spunkycharts.traditionalchart.draw.DrawSemantics_CANDLE.java
com.jogden.spunkycharts.traditionalchart.draw.DrawSemantics_LINE.java
com.jogden.spunkycharts.traditionalchart.draw.DrawSemantics_OC.java
com.jogden.spunkycharts.traditionalchart.draw.DrawSemantics_OHLC.java
com.jogden.spunkycharts.traditionalchart.draw.DrawSemantics_POINT.java
com.jogden.spunkycharts.traditionalchart.draw.DrawSemantics_SILO.java