/***************************************************************** BioZen Copyright (C) 2011 The National Center for Telehealth and Technology Eclipse Public License 1.0 (EPL-1.0) This library is free software; you can redistribute it and/or modify it under the terms of the Eclipse Public License as published by the Free Software Foundation, version 1.0 of the License. The Eclipse Public License is a reciprocal license, under Section 3. REQUIREMENTS iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. Post your updates and modifications to our GitHub or email to t2@tee2.org. This library is distributed WITHOUT ANY WARRANTY; without the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the Eclipse Public License 1.0 (EPL-1.0) for more details. You should have received a copy of the Eclipse Public License along with this library; if not, visit http://www.opensource.org/licenses/EPL-1.0 *****************************************************************/ package com.t2.compassionMeditation; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Timer; import java.util.TimerTask; import java.util.Vector; import org.achartengine.ChartFactory; import org.achartengine.GraphicalView; import org.achartengine.chart.PointStyle; import org.achartengine.model.XYMultipleSeriesDataset; import org.achartengine.renderer.XYMultipleSeriesRenderer; import org.achartengine.renderer.XYSeriesRenderer; import bz.org.t2health.lib.activity.BaseActivity; import com.t2.Constants; import spine.SPINEFunctionConstants; import spine.datamodel.Node; import spine.datamodel.Data; import spine.datamodel.MindsetData; import spine.datamodel.ZephyrData; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.AssetManager; import android.content.res.Resources; import android.graphics.Color; import android.graphics.PorterDuff; import android.os.Bundle; import android.os.Environment; import android.preference.PreferenceManager; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.style.ForegroundColorSpan; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.Window; import android.view.ViewGroup.LayoutParams; import android.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; import android.widget.TextView; //Need the following import to get access to the app resources, since this //class is in a sub-package. import com.t2.R; public class ViewHistoryActivity extends BaseActivity implements OnSeekBarChangeListener { private static final String TAG = "BFDemo"; private static final String mActivityVersion = "1.0"; private BufferedReader mLogReader = null; private Vector mSessionData; private int mCursor = 0; /** * Session name which is used for file creation (based on selected user) */ private String mSessionName = ""; /** * Application version info determined by the package manager */ private String mApplicationVersion = ""; /** * Static instance of this activity */ private static ViewHistoryActivity instance; /** * Timer for updating the UI */ private static Timer mDataUpdateTimer; // Charting stuff private final static int SPINE_CHART_SIZE = 20; private GraphicalView mDeviceChartView; private int mSpineChartX = 0; private boolean mPaused = false; // UI Elements private Button mAddMeasureButton; private Button mPauseButton; private Button mToggleLogButton; private Button mLlogMarkerButton; private TextView mTextInfoView; private TextView mTextViewComment; private TextView mMeasuresDisplayText; private SeekBar mSeekBar; protected SharedPreferences sharedPref; private static final String KEY_NAME = "results_visible_ids_"; private ArrayList<KeyItem> keyItems = new ArrayList<KeyItem>(); private MindsetData currentMindsetData; private int bandOfInterest = MindsetData.THETA_ID; // Default to theta private int numSecsWithoutData = 0; private int heartRatePos; private int respRatePos; private int skinTempPos; /** * @return Static instance of this activity */ public static ViewHistoryActivity getInstance() { return instance; } /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.requestWindowFeature(Window.FEATURE_NO_TITLE); // This needs to happen BEFORE setContentView setContentView(R.layout.view_history); instance = this; currentMindsetData = new MindsetData(this); sharedPref = PreferenceManager.getDefaultSharedPreferences(getBaseContext()); setRequestedOrientation (ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); String s =SharedPref.getString(this, BioZenConstants.PREF_BAND_OF_INTEREST ,"0"); bandOfInterest = Integer.parseInt(s); Resources resources = this.getResources(); AssetManager assetManager = resources.getAssets(); // Set up member variables to UI Elements mPauseButton = (Button) findViewById(R.id.buttonPause); mAddMeasureButton = (Button) findViewById(R.id.buttonAddMeasure); // mToggleLogButton = (Button) findViewById(R.id.buttonLogging); // mLlogMarkerButton = (Button) findViewById(R.id.LogMarkerButton); mTextInfoView = (TextView) findViewById(R.id.textViewInfo); mTextViewComment = (TextView) findViewById(R.id.textViewComment); mMeasuresDisplayText = (TextView) findViewById(R.id.measuresDisplayText); ImageView image = (ImageView) findViewById(R.id.imageView1); image.setImageResource(R.drawable.signal_bars0); mSeekBar = (SeekBar)findViewById(R.id.seekBar1); mSeekBar.setOnSeekBarChangeListener(this); int i; for (i = 0; i < MindsetData.NUM_BANDS + 2; i++) { // 2 extra, for attention and meditation KeyItem key = new KeyItem(i, MindsetData.spectralNames[i], ""); keyItems.add(key); } heartRatePos = i; KeyItem key = new KeyItem(i++, "HeartRate", ""); keyItems.add(key); respRatePos = i; key = new KeyItem(i++, "RespRate", ""); keyItems.add(key); skinTempPos = i; key = new KeyItem(i, "SkinTemp", ""); keyItems.add(key); // Set up Device data chart generateChart(); try { PackageManager packageManager = this.getPackageManager(); PackageInfo info = packageManager.getPackageInfo(this.getPackageName(), 0); mApplicationVersion = info.versionName; Log.i(TAG, "BioZen Application Version: " + mApplicationVersion + ", Activity Version: " + mActivityVersion); } catch (NameNotFoundException e) { Log.e(TAG, e.toString()); } // Get the session(file) name try { // Get target name if one was supplied Bundle bundle = getIntent().getExtras(); mSessionName = bundle.getString(BioZenConstants.EXTRA_SESSION_NAME); mSessionData = new Vector(); loadSessionData(); mSeekBar.setMax(mSessionData.size() - SPINE_CHART_SIZE); updateChart(); } catch (Exception e1) { mSessionName = ""; } } // End onCreate(Bundle savedInstanceState) class MindsetPoint extends MindsetData { /** * */ private static final long serialVersionUID = 5647398490731023479L; public String dateTime; public String comment; public ZephyrData zephyrData = new ZephyrData();; @Override public String getSpectralName(int band) { if (band >= MindsetData.NUM_BANDS + 2) { return zephyrData.getParameterName(band); } else { return super.getSpectralName(band); } } @Override public int getFeatureValue(int band) { if (band >= MindsetData.NUM_BANDS + 2) { return zephyrData.getFeatureValue(band); } else { return super.getFeatureValue(band); } } MindsetPoint() { super(context); } } void updateChart() { for (int j = 0; j < SPINE_CHART_SIZE; j++) { if (j + mCursor >= mSessionData.size()) { if (mDeviceChartView != null) { mDeviceChartView.repaint(); } return; } MindsetPoint p = (MindsetPoint) mSessionData.get(j + mCursor); if (p.comment.equalsIgnoreCase("")) { MindsetPoint data = (MindsetPoint) mSessionData.get(j + mCursor); currentMindsetData = (MindsetPoint) mSessionData.get(j + mCursor); for (int i = 0; i < MindsetData.NUM_BANDS + 2; i++) { // 2 extra, for attention and meditation keyItems.get(i).rawValue = currentMindsetData.getFeatureValue(i); } keyItems.get(heartRatePos).rawValue = data.zephyrData.heartRate / 3; keyItems.get(respRatePos).rawValue = data.zephyrData.respRate * 5; keyItems.get(skinTempPos).rawValue = data.zephyrData.skinTemp; int keyCount = keyItems.size(); for(int i = 0; i < keyItems.size(); ++i) { KeyItem item = keyItems.get(i); if(!item.visible) { continue; } int v = currentMindsetData.getFeatureValue((int) item.id); // item.series.add(mSpineChartX, currentMindsetData.getFeatureValue((int) item.id)); item.xySeries.add(mSpineChartX, item.rawValue); if (item.xySeries.getItemCount() > SPINE_CHART_SIZE) { item.xySeries.remove(0); } mSpineChartX++; } } else { // It's a comment } } if (mDeviceChartView != null) { mDeviceChartView.repaint(); } } MindsetPoint parseLine(String line) { MindsetPoint data = new MindsetPoint(); String[] tokens = line.split(","); if (tokens.length == 2) { data.dateTime = tokens[0]; data.comment = tokens[1]; } else if (tokens.length == 23) { int i = 0; data.dateTime = tokens[i++]; data.comment = tokens[i++]; data.poorSignalStrength = Integer.parseInt(tokens[i++].trim()); data.attention = Integer.parseInt(tokens[i++].trim()); data.meditation = Integer.parseInt(tokens[i++].trim()); for (int j = 0; j < MindsetData.NUM_BANDS; j++) { data.scaledSpectralData[j] = Integer.parseInt(tokens[i++].trim()); } i++; for (int j = 0; j < MindsetData.NUM_BANDS; j++) { data.rawSpectralData[j] = Integer.parseInt(tokens[i++].trim()); } } else if (tokens.length == 25) { int i = 0; data.dateTime = tokens[i++]; data.zephyrData.heartRate = Integer.parseInt(tokens[i++].trim()); data.zephyrData.respRate = Integer.parseInt(tokens[i++].trim()); data.zephyrData.skinTemp = Integer.parseInt(tokens[i++].trim()); data.comment = ""; data.poorSignalStrength = Integer.parseInt(tokens[i++].trim()); data.attention = Integer.parseInt(tokens[i++].trim()); data.meditation = Integer.parseInt(tokens[i++].trim()); for (int j = 0; j < MindsetData.NUM_BANDS; j++) { data.ratioSpectralData[j] = Integer.parseInt(tokens[i++].trim()); } i++; for (int j = 0; j < MindsetData.NUM_BANDS; j++) { data.rawSpectralData[j] = Integer.parseInt(tokens[i++].trim()); } } return data; } boolean loadSessionData() { // Open a file for saving data try { File root = Environment.getExternalStorageDirectory(); if (root.canWrite()){ File gpxfile = new File(root, mSessionName); FileReader gpxreader = new FileReader(gpxfile); // open for append mLogReader = new BufferedReader(gpxreader); String lineToParse; while ((lineToParse = mLogReader.readLine()) != null) { try { MindsetPoint point = parseLine(lineToParse); mSessionData.add(point); Log.i(TAG,lineToParse); } catch (NumberFormatException e) { // TODO Auto-generated catch block e.printStackTrace(); } } TextView textViewSessionName = (TextView) findViewById(R.id.textViewSessionName); textViewSessionName.setText("Session: " + mSessionName); } else { Log.e(TAG, "Could not open file " ); AlertDialog.Builder alert = new AlertDialog.Builder(this); alert.setTitle("ERROR"); alert.setMessage("Cannot open to file"); alert.show(); } } catch (IOException e) { Log.e(TAG, "Could not write file " + e.getMessage()); AlertDialog.Builder alert = new AlertDialog.Builder(this); alert.setTitle("ERROR"); alert.setMessage("Cannot write to file"); alert.show(); } return true; } @Override protected void onDestroy() { super.onDestroy(); saveState(); Log.i(TAG, TAG + " onDestroy"); } private void generateChart() { // Set up chart XYMultipleSeriesDataset deviceDataset = new XYMultipleSeriesDataset(); XYMultipleSeriesRenderer deviceRenderer = new XYMultipleSeriesRenderer(); LinearLayout layout = (LinearLayout) findViewById(R.id.deviceChart); if (mDeviceChartView != null) { layout.removeView(mDeviceChartView); } if (true) { mDeviceChartView = ChartFactory.getLineChartView(this, deviceDataset, deviceRenderer); mDeviceChartView.setBackgroundColor(Color.BLACK); // mDeviceChartView.setBackgroundColor(Color.WHITE); layout.addView(mDeviceChartView, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); } deviceRenderer.setShowLabels(false); deviceRenderer.setMargins(new int[] {0,5,5,0}); deviceRenderer.setShowAxes(true); deviceRenderer.setShowLegend(false); deviceRenderer.setZoomEnabled(false, false); deviceRenderer.setPanEnabled(false, false); deviceRenderer.setYAxisMin(0); deviceRenderer.setYAxisMax(150); // deviceRenderer.setYAxisMax(255); SpannableStringBuilder sMeasuresText = new SpannableStringBuilder("Displaying: "); ArrayList<Long> visibleIds = getVisibleIds("measure"); int keyCount = keyItems.size(); keyCount = keyItems.size(); int lineNum = 0; for(int i = 0; i < keyItems.size(); ++i) { KeyItem item = keyItems.get(i); item.visible = visibleIds.contains(item.id); if(!item.visible) { continue; } deviceDataset.addSeries(item.xySeries); item.color = getKeyColor(i, keyCount); // Add name of the measure to the displayed text field ForegroundColorSpan fcs = new ForegroundColorSpan(item.color); int start = sMeasuresText.length(); sMeasuresText.append(keyItems.get(i).title1 + ", "); int end = sMeasuresText.length(); sMeasuresText.setSpan(fcs, start, end, Spannable.SPAN_INCLUSIVE_INCLUSIVE); if (sMeasuresText.length() > 40 && lineNum == 0) { lineNum++; } XYSeriesRenderer seriesRenderer = new XYSeriesRenderer(); seriesRenderer.setColor(item.color); seriesRenderer.setPointStyle(PointStyle.CIRCLE); // seriesRenderer.setFillPoints(true); // seriesRenderer.setLineWidth(2 * displayMetrics.density); deviceRenderer.addSeriesRenderer(seriesRenderer); } mMeasuresDisplayText.setText(sMeasuresText) ; } @Override protected void onStart() { super.onStart(); Log.i(TAG, TAG + " OnStart"); // Set up a timer to do graphical updates mDataUpdateTimer = new Timer(); mDataUpdateTimer.schedule(new TimerTask() { @Override public void run() { TimerMethod(); } }, 0, 1000); setCursor(0); } @Override public boolean onCreateOptionsMenu(Menu menu) { this.getMenuInflater().inflate(R.menu.menu_compassion_meditation, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch(item.getItemId()) { case R.id.about: String content = "National Center for Telehealth and Technology (T2)\n\n"; content += "BioZen Application\n"; content += "Application Version: " + mApplicationVersion + "\n"; content += "Activity Version: " + mActivityVersion; AlertDialog.Builder alert = new AlertDialog.Builder(this); alert.setTitle("About"); alert.setMessage(content); alert.show(); return true; default: return super.onOptionsItemSelected(item); } } /** * This is where we receive sensor data that comes through the actual * Spine channel. * @param data Generic Spine data packet. Should be cast to specifid data type indicated by data.getFunctionCode() * * @see spine.SPINEListener#received(spine.datamodel.Data) */ public void received(Data data) { if (data != null) { switch (data.getFunctionCode()) { case SPINEFunctionConstants.MINDSET: { Node source = data.getNode(); MindsetData mindsetData = (MindsetData) data; if (mindsetData.exeCode == Constants.EXECODE_POOR_SIG_QUALITY) { int sigQuality = mindsetData.poorSignalStrength & 0xff; ImageView image = (ImageView) findViewById(R.id.imageView1); if (sigQuality == 200) image.setImageResource(R.drawable.signal_bars0); else if (sigQuality > 150) image.setImageResource(R.drawable.signal_bars1); else if (sigQuality > 100) image.setImageResource(R.drawable.signal_bars2); else if (sigQuality > 50) image.setImageResource(R.drawable.signal_bars3); else if (sigQuality > 25) image.setImageResource(R.drawable.signal_bars4); else image.setImageResource(R.drawable.signal_bars5); } if (mindsetData.exeCode == Constants.EXECODE_SPECTRAL) { currentMindsetData.updateSpectral(mindsetData); Log.i(TAG, "Spectral Data"); numSecsWithoutData = 0; } if (mindsetData.exeCode == Constants.EXECODE_POOR_SIG_QUALITY) { currentMindsetData.poorSignalStrength = mindsetData.poorSignalStrength; } if (mindsetData.exeCode == Constants.EXECODE_ATTENTION) { currentMindsetData.attention= mindsetData.attention; } if (mindsetData.exeCode == Constants.EXECODE_MEDITATION) { currentMindsetData.meditation= mindsetData.meditation; } break; } // End case SPINEFunctionConstants.MINDSET: } // End switch (data.getFunctionCode()) } // End if (data != null) } /** * Converts a byte array to an integer * @param bytes Bytes to convert * @return Integer representaion of byte array */ public static int byteArrayToInt(byte[] bytes) { int val = 0; for(int i = 0; i < bytes.length; i++) { int n = (bytes[i] < 0 ? (int)bytes[i] + 256 : (int)bytes[i]) << (8 * i); val += n; } return val; } public void onButtonClick(View v) { final int id = v.getId(); switch (id) { case R.id.buttonLeft: if (mCursor > 0) { mSeekBar.setProgress(mCursor - 1); } break; case R.id.buttonRight: mSeekBar.setProgress(mCursor + 1); // A HUGE cheat here, if we're as far as we can go then show the comment from the last line if (mCursor >= mSessionData.size() - SPINE_CHART_SIZE) { MindsetPoint p = (MindsetPoint) mSessionData.get(mSessionData.size() - 1); if (!p.comment.equalsIgnoreCase("")) { mTextViewComment.setText("Comment: " + p.comment); } else { mTextViewComment.setText(""); } } break; case R.id.buttonBack: finish(); break; case R.id.buttonAddMeasure: boolean toggleArray[] = new boolean[keyItems.size() + 2]; for(int j = 0; j < keyItems.size(); ++j) { KeyItem item = keyItems.get(j); if(item.visible) toggleArray[j] = true; else toggleArray[j] = false; } String[] measureNames = new String[keyItems.size()]; int i = 0; for (KeyItem item: keyItems) { measureNames[i++] = item.title1; } AlertDialog.Builder alert = new AlertDialog.Builder(this); alert.setTitle(R.string.alert_dialog_measure_selector); alert.setMultiChoiceItems(measureNames, // alert.setMultiChoiceItems(R.array.measure_select_dialog_items, toggleArray, new DialogInterface.OnMultiChoiceClickListener() { public void onClick(DialogInterface dialog, int whichButton,boolean isChecked) { KeyItem item = keyItems.get(whichButton); item.visible = item.visible ? false: true; saveVisibleKeyIds(); generateChart(); } }); alert.setPositiveButton(R.string.alert_dialog_ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { generateChart(); } }); alert.show(); break; case R.id.buttonPause: if (mPaused == true) { mPaused = false; mPauseButton.getBackground().setColorFilter(Color.LTGRAY, PorterDuff.Mode.MULTIPLY); } else { mPaused = true; mPauseButton.getBackground().setColorFilter(0xFFFF0000, PorterDuff.Mode.MULTIPLY); } break; } // End switch } /** * This method is called directly by the timer and runs in the same thread as the timer * From here We call the method that will work with the UI through the runOnUiThread method. */ private void TimerMethod() { this.runOnUiThread(Timer_Tick); } /** * This method runs in the same thread as the UI. */ private Runnable Timer_Tick = new Runnable() { public void run() { } }; @Override protected void onPause() { Log.i(TAG, TAG + " onPause"); mDataUpdateTimer.purge(); mDataUpdateTimer.cancel(); saveState(); super.onPause(); } @Override protected void onStop() { Log.i(TAG, TAG + " onStop"); super.onStop(); } @Override protected void onRestart() { Log.i(TAG, TAG + " onRestart"); super.onRestart(); } @Override protected void onResume() { Log.i(TAG, TAG + " onResume"); restoreState(); super.onResume(); } void saveState() { } void restoreState() { } private void saveVisibleKeyIds() { String keySuffix = "measure"; ArrayList<Long> toggledIds = new ArrayList<Long>(); for(int i = 0; i < keyItems.size(); ++i) { KeyItem item = keyItems.get(i); if(item.visible) { toggledIds.add(item.id); } } setVisibleIds(keySuffix, toggledIds); } private ArrayList<Long> getVisibleIds(String keySuffix) { String[] idsStrArr = SharedPref.getValues( sharedPref, KEY_NAME+keySuffix, ",", new String[0] ); return new ArrayList<Long>( Arrays.asList( ArraysExtra.toLongArray(idsStrArr) ) ); } private void setVisibleIds(String keySuffix, ArrayList<Long> ids) { SharedPref.setValues( sharedPref, KEY_NAME+keySuffix, ",", ArraysExtra.toStringArray(ids.toArray(new Long[ids.size()])) ); } protected int getKeyColor(int currentIndex, int totalCount) { float hue = currentIndex / (1.00f * totalCount) * 360.00f; return Color.HSVToColor( 255, new float[]{ hue, 1.0f, 1.0f } ); } @Override public void onProgressChanged(SeekBar arg0, int arg1, boolean arg2) { try { setCursor(arg1); } catch (Exception e) { e.printStackTrace(); } } void setCursor(int start) { mCursor = start; MindsetPoint p = (MindsetPoint) mSessionData.get(mCursor); mTextInfoView.setText(p.dateTime + ":\n " + p.getSpectralName(bandOfInterest) + ":" + p.getFeatureValue(bandOfInterest) + "\n"); if (!p.comment.equalsIgnoreCase("")) { mTextViewComment.setText("Comment: " + p.comment); } else { mTextViewComment.setText(""); } updateChart(); } @Override public void onStartTrackingTouch(SeekBar arg0) { // TODO Auto-generated method stub } @Override public void onStopTrackingTouch(SeekBar arg0) { // TODO Auto-generated method stub } }