diff --git a/android/ConductorsPhone/app/build.gradle b/android/ConductorsPhone/app/build.gradle index d208ac1..993b6df 100644 --- a/android/ConductorsPhone/app/build.gradle +++ b/android/ConductorsPhone/app/build.gradle @@ -8,10 +8,11 @@ android { compileSdkVersion 26 defaultConfig { applicationId "de.tonifetzer.conductorswatch" - minSdkVersion 23 + minSdkVersion 24 targetSdkVersion 26 - versionCode 1 - versionName "1.0" + //sdk 2 | product version 3 | build num 2 | multi-apk 2 + versionCode 260120100 + versionName "0.1.2" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { @@ -20,6 +21,10 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } dependencies { diff --git a/android/ConductorsPhone/app/src/main/AndroidManifest.xml b/android/ConductorsPhone/app/src/main/AndroidManifest.xml index 4bf6a67..4c4f298 100644 --- a/android/ConductorsPhone/app/src/main/AndroidManifest.xml +++ b/android/ConductorsPhone/app/src/main/AndroidManifest.xml @@ -2,6 +2,9 @@ + + + + diff --git a/android/ConductorsPhone/app/src/main/java/de/tonifetzer/conductorswatch/DataFolder.java b/android/ConductorsPhone/app/src/main/java/de/tonifetzer/conductorswatch/DataFolder.java new file mode 100644 index 0000000..1d4f446 --- /dev/null +++ b/android/ConductorsPhone/app/src/main/java/de/tonifetzer/conductorswatch/DataFolder.java @@ -0,0 +1,57 @@ +package de.tonifetzer.conductorswatch; + +/** + * Created by toni on 12/01/18. + */ + +import android.content.Context; +import android.os.Environment; +import android.util.Log; + +import java.io.File; + +/** + * Class: DataFolder + * Description: SDK save file class. Is able to open a folder on the device independent of the given + * device and android skd version. + */ +public class DataFolder { + + private File folder; + private static final String TAG = "DataFolder"; + + public DataFolder(Context context, String folderName){ + + // 1) try external data folder + folder = new File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), folderName); + if (isOK(folder)) {return;} + + // 2) try sd-card folder + folder = new File(Environment.getExternalStorageDirectory() + "/" + folderName); + if (isOK(folder)) {return;} + + // 3) try internal data folder + folder = new File(context.getApplicationInfo().dataDir); + if (isOK(folder)) {return;} + + // all failed + throw new RuntimeException("failed to create/access storage folder"); + + } + + /** ensure the given folder is OK */ + private static final boolean isOK(final File folder) { + folder.mkdirs(); + final boolean ok = folder.exists() && folder.isDirectory(); + if (ok) { + Log.d(TAG, "using: " + folder); + } else { + Log.d(TAG, "not OK: " + folder); + } + return ok; + } + + public File getFolder(){ + return folder; + } +} diff --git a/android/ConductorsPhone/app/src/main/java/de/tonifetzer/conductorswatch/DataLayerListenerService.java b/android/ConductorsPhone/app/src/main/java/de/tonifetzer/conductorswatch/DataLayerListenerService.java index 821daa3..755bc37 100644 --- a/android/ConductorsPhone/app/src/main/java/de/tonifetzer/conductorswatch/DataLayerListenerService.java +++ b/android/ConductorsPhone/app/src/main/java/de/tonifetzer/conductorswatch/DataLayerListenerService.java @@ -3,10 +3,17 @@ package de.tonifetzer.conductorswatch; import android.content.Intent; import android.util.Log; import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.wearable.ChannelClient; import com.google.android.gms.wearable.MessageEvent; import com.google.android.gms.wearable.Wearable; import com.google.android.gms.wearable.WearableListenerService; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + /** * Listens to DataItems and Messages from the local node. */ @@ -14,6 +21,7 @@ public class DataLayerListenerService extends WearableListenerService { private static final String TAG = "DataLayerService"; private static final String START_ACTIVITY_PATH = "/start-activity"; + private static final String DATA_PATH ="/data"; GoogleApiClient mGoogleApiClient; @@ -28,7 +36,7 @@ public class DataLayerListenerService extends WearableListenerService { @Override public void onMessageReceived(MessageEvent messageEvent) { - //LOGD(TAG, "onMessageReceived: " + messageEvent); + LOGD(TAG, "onMessageReceived: " + messageEvent); // Check to see if the message is to start an activity if (messageEvent.getPath().equals(START_ACTIVITY_PATH)) { @@ -43,4 +51,40 @@ public class DataLayerListenerService extends WearableListenerService { Log.d(tag, message); } } + + @Override + public void onOutputClosed(ChannelClient.Channel openChannel, int i, int i1){ + + LOGD(TAG, "Channel successfully opened."); + + //get the sensordata provided by the watch + Wearable.getChannelClient(this).getInputStream(openChannel).addOnSuccessListener((InputStream stream) -> { + + Log.d(TAG, "Channel successfully opened and ready to write file."); + + byte[] data = null; + try { + stream.read(data); + DataFolder folder = new DataFolder(this, "sensorOutFiles"); + File file = new File(folder.getFolder(), System.currentTimeMillis() + ".csv"); + + try { + FileOutputStream fos = new FileOutputStream(file); + fos.write(data); + fos.close(); + + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + + } catch (IOException e) { + e.printStackTrace(); + } + }).addOnFailureListener((Exception e) -> { + //Channel not open? couldn't receive inputstream? + }); + + //we have what we wanted, so close the channel + Wearable.getChannelClient(this).close(openChannel); + } } \ No newline at end of file diff --git a/android/ConductorsPhone/app/src/main/java/de/tonifetzer/conductorswatch/MainActivity.java b/android/ConductorsPhone/app/src/main/java/de/tonifetzer/conductorswatch/MainActivity.java index 735697b..b813237 100644 --- a/android/ConductorsPhone/app/src/main/java/de/tonifetzer/conductorswatch/MainActivity.java +++ b/android/ConductorsPhone/app/src/main/java/de/tonifetzer/conductorswatch/MainActivity.java @@ -1,22 +1,25 @@ package de.tonifetzer.conductorswatch; +import android.app.Activity; import android.graphics.Color; -import android.media.MediaPlayer; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; -import android.view.SoundEffectConstants; +import android.widget.EditText; import android.widget.TextView; +import com.google.android.gms.wearable.ChannelClient; import com.google.android.gms.wearable.MessageEvent; import com.google.android.gms.wearable.MessageClient; import com.google.android.gms.wearable.Wearable; - import java.util.Timer; +import de.tonifetzer.conductorswatch.network.SensorDataCallback; + public class MainActivity extends AppCompatActivity implements MessageClient.OnMessageReceivedListener{ private TextView mTextView; - Timer mTimer; + private EditText mEditView; + private Timer mTimer; private static final String TAG = "DataLayerService"; private static final String START_ACTIVITY_PATH = "/start-activity"; @@ -26,27 +29,34 @@ public class MainActivity extends AppCompatActivity implements MessageClient.OnM private boolean mIsRecording = false; + private ChannelClient.ChannelCallback mChannelCallback; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - mTextView = (TextView) findViewById(R.id.bpmText); + mTextView = findViewById(R.id.bpmText); + mEditView = findViewById(R.id.comments); + + // handels everything regarding sensor data delivered by the watch. + mChannelCallback = new SensorDataCallback(this); } @Override public void onResume() { super.onResume(); Wearable.getMessageClient(this).addListener(this); + Wearable.getChannelClient(this).registerChannelCallback(mChannelCallback); } @Override protected void onPause() { super.onPause(); Wearable.getMessageClient(this).removeListener(this); + Wearable.getChannelClient(this).unregisterChannelCallback(mChannelCallback); } - @Override public void onMessageReceived(MessageEvent messageEvent) { @@ -67,6 +77,8 @@ public class MainActivity extends AppCompatActivity implements MessageClient.OnM mTimer = new Timer(); mTimer.scheduleAtFixedRate(new Metronome(this) , 0, 60000 / Integer.parseInt(parts[1])); mTextView.setTextColor(Color.parseColor("#EE693F")); + mEditView.setEnabled(false); + mEditView.setFocusable(false); mIsRecording = true; } @@ -75,6 +87,9 @@ public class MainActivity extends AppCompatActivity implements MessageClient.OnM mTimer.cancel(); mTextView.setTextColor(Color.parseColor("#158b69")); + mEditView.setEnabled(true); + mEditView.setFocusable(true); + mEditView.setFocusableInTouchMode(true); mIsRecording = false; } else if (messageEvent.getPath().contains(UPDATE_PATH)){ diff --git a/android/ConductorsPhone/app/src/main/java/de/tonifetzer/conductorswatch/network/SensorDataCallback.java b/android/ConductorsPhone/app/src/main/java/de/tonifetzer/conductorswatch/network/SensorDataCallback.java new file mode 100644 index 0000000..1f2ecd9 --- /dev/null +++ b/android/ConductorsPhone/app/src/main/java/de/tonifetzer/conductorswatch/network/SensorDataCallback.java @@ -0,0 +1,68 @@ +package de.tonifetzer.conductorswatch.network; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.util.Log; + +import com.google.android.gms.wearable.ChannelClient; +import com.google.android.gms.wearable.Wearable; + +import java.io.InputStream; + +/** + * Created by toni on 14/01/18. + */ + +public class SensorDataCallback extends ChannelClient.ChannelCallback implements SensorDataFileReceiver.OnFileReceiveListener{ + + private static final String TAG = "SensorDataCallback"; + + private Context mContext; + private SensorDataFileReceiver mFileReceiver; + private ChannelClient.Channel mOpenChannel; + + public SensorDataCallback(Context context) { + mContext = context; + mOpenChannel = null; + + mFileReceiver = new SensorDataFileReceiver(mContext); + mFileReceiver.add(this); + } + + @Override + public void onChannelOpened(@NonNull ChannelClient.Channel openChannel) { + super.onChannelOpened(openChannel); + + mOpenChannel = openChannel; + Wearable.getChannelClient(mContext).getInputStream(mOpenChannel).addOnSuccessListener((InputStream stream) -> { + + mFileReceiver.setOpenStream(stream); + new Thread(mFileReceiver, "FileReceiverThread").start(); + }); + } + + @Override + public void onChannelClosed(@NonNull ChannelClient.Channel openChannel, int closeReason, int appSpecificErrorCode) { + super.onChannelClosed(openChannel, closeReason, appSpecificErrorCode); + + Log.d(TAG, "Channel is closed. Reason: " + closeReason + " - ErrorCode: " + appSpecificErrorCode); + } + + @Override + public void onStartSaveFile() { + //TODO: runOnUIThread stuff + + Log.d(TAG, "Start saving file. - " + Thread.currentThread().getName()); + } + + @Override + public void onFinishedSavingFile() { + + //close the channel + if(mOpenChannel != null){ + Wearable.getChannelClient(mContext).close(mOpenChannel); + } + + Log.d(TAG, "Finished saving file. - " + Thread.currentThread().getName()); + } +} diff --git a/android/ConductorsPhone/app/src/main/java/de/tonifetzer/conductorswatch/network/SensorDataFileReceiver.java b/android/ConductorsPhone/app/src/main/java/de/tonifetzer/conductorswatch/network/SensorDataFileReceiver.java new file mode 100644 index 0000000..db9526c --- /dev/null +++ b/android/ConductorsPhone/app/src/main/java/de/tonifetzer/conductorswatch/network/SensorDataFileReceiver.java @@ -0,0 +1,111 @@ +package de.tonifetzer.conductorswatch.network; + +import android.content.Context; +import android.util.Log; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Created by toni on 14/01/18. + */ + +public class SensorDataFileReceiver implements Runnable { + + private InputStream mOpenStream; + private Context mContext; + private static final String TAG = "SensorDataFileReceiver"; + + public SensorDataFileReceiver(Context context){ + mOpenStream = null; + mContext = context; + } + + public void setOpenStream(InputStream stream){ + mOpenStream = stream; + } + + @Override + public void run() { + + if(mOpenStream != null){ + + Log.d(TAG, "Opened InputStream in Thread: " + Thread.currentThread().getName()); + + for(SensorDataFileReceiver.OnFileReceiveListener listener : listeners){ + listener.onStartSaveFile(); + } + + byte[] bytesTimestamp = new byte[Long.BYTES]; + byte[] bytesSensorID = new byte[Integer.BYTES]; + byte[] bytesX= new byte[Double.BYTES]; + byte[] bytesY= new byte[Double.BYTES]; + byte[] bytesZ= new byte[Double.BYTES]; + + long ts = 0; + int id = 0; + double x = 0, y = 0, z = 0; + + int i = 0; + SensorDataFileWriter fileWriter = new SensorDataFileWriter(mContext); + + try { + while(mOpenStream.read(bytesTimestamp) != -1) { + ts = ByteBuffer.wrap(bytesTimestamp).getLong(); + + mOpenStream.read(bytesSensorID); + id = ByteBuffer.wrap(bytesSensorID).getInt(); + + mOpenStream.read(bytesX); + x = ByteBuffer.wrap(bytesX).getDouble(); + + mOpenStream.read(bytesY); + y = ByteBuffer.wrap(bytesY).getDouble(); + + mOpenStream.read(bytesZ); + z = ByteBuffer.wrap(bytesZ).getDouble(); + + ++i; + + if(id != 4 && id != 10){ + Log.d(TAG, "STOOOOOP"); + } + + fileWriter.writeVector3D(ts, id, x, y, z); + } + + //we have what we wanted, so close the channel + fileWriter.toDisk(); + Log.d(TAG, "Num Data transferred: " + i); + + //finished callback + for(SensorDataFileReceiver.OnFileReceiveListener listener : listeners){ + listener.onFinishedSavingFile(); + } + + //close the stream, since this is recommended + mOpenStream.close(); + + } catch (IOException e) { + e.printStackTrace(); + } + } else { + Log.d(TAG, "Channel is not valid, is it really open? Check connection to watch! - Thread: " + Thread.currentThread().getName()); + } + } + + + /** + * Interface for callback + */ + public interface OnFileReceiveListener { + void onStartSaveFile(); + void onFinishedSavingFile(); + } + + private List listeners = new CopyOnWriteArrayList(); + public void add(SensorDataFileReceiver.OnFileReceiveListener listener){listeners.add(listener);} + public void remove(SensorDataFileReceiver.OnFileReceiveListener listener){listeners.remove(listener);} +} diff --git a/android/ConductorsPhone/app/src/main/java/de/tonifetzer/conductorswatch/network/SensorDataFileWriter.java b/android/ConductorsPhone/app/src/main/java/de/tonifetzer/conductorswatch/network/SensorDataFileWriter.java new file mode 100644 index 0000000..6780856 --- /dev/null +++ b/android/ConductorsPhone/app/src/main/java/de/tonifetzer/conductorswatch/network/SensorDataFileWriter.java @@ -0,0 +1,139 @@ +package de.tonifetzer.conductorswatch.network; + +import android.app.Activity; +import android.content.Context; +import android.os.AsyncTask; +import android.util.Log; +import android.widget.EditText; +import android.widget.TextView; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +import de.tonifetzer.conductorswatch.DataFolder; +import de.tonifetzer.conductorswatch.R; + +/** + * Created by toni on 14/01/18. + */ + +public class SensorDataFileWriter { + + private static final int FLUSH_LIMIT = 2*1024*1024; + private static final String TAG = "SensorDataFileWriter"; + + private File mSensorDataFile; + private FileOutputStream mFileOutputStream; + private StringBuilder mStringBuilder = new StringBuilder(); + private final DataFolder mFolder; + private boolean mStreamOpenend = false; + + public SensorDataFileWriter(Context context) { + + mFolder = new DataFolder(context, "sensorOutFiles"); + + //write some description for this file in the first line + EditText editView = ((Activity) context).findViewById(R.id.comments); + TextView textView = ((Activity) context).findViewById(R.id.bpmText); + writeDescription("Kommentar: " + editView.getText().toString() + "\nMetronom: " + textView.getText().toString() + " bpm"); + + //open connection to file + open(); + } + + public final void writeVector3D(long ts, int id, double x, double y, double z){ + + synchronized (this) { + + if (mStreamOpenend) { + + mStringBuilder.append(ts).append(';').append(id).append(';').append(x).append(';').append(y).append(';').append(z).append('\n'); + + if (mStringBuilder.length() > FLUSH_LIMIT) {flush(false);} + } else { + open(); + } + } + } + + private final void writeDescription(String str){ + synchronized (this) { + mStringBuilder.append(str).append('\n').append('\n'); + } + + } + + // flush data to disk + public final void toDisk(){ + synchronized (this) { + flush(true); + close(); + } + } + + /** helper method for exception-less writing. DO NOT CALL DIRECTLY! */ + private final void _write(final byte[] data) { + try { + mFileOutputStream.write(data); + Log.d(TAG, "flushed " + data.length + " bytes to disk"); + } catch (final Exception e) { + throw new RuntimeException("error while writing log-file", e); + } + } + + /** helper-class for background writing */ + class FlushAsync extends AsyncTask { + @Override + protected final Integer doInBackground(byte[][] data) { + _write(data[0]); + return null; + } + }; + + /** flush current buffer-contents to disk */ + private final void flush(boolean sync) { + + // fetch current buffer contents to write and hereafter empty the buffer + // this action MUST be atomic, just like the add-method + byte[] data = null; + synchronized (this) { + data = mStringBuilder.toString().getBytes(); // fetch data to write + mStringBuilder.setLength(0); // reset the buffer + } + + // write + if (sync) { + // write to disk using the current thread + _write(data); + } else { + // write to disk using a background-thread + new FlushAsync().execute(new byte[][] {data}); + } + } + + private final void open() { + mSensorDataFile = new File(mFolder.getFolder(), System.currentTimeMillis() + ".csv"); + + try { + mFileOutputStream = new FileOutputStream(mSensorDataFile); + mStreamOpenend = true; + + Log.d(TAG, "will write to: " + mSensorDataFile.toString()); + } catch (FileNotFoundException e) { + e.printStackTrace(); + + mStreamOpenend = false; + } + } + + private final void close() { + try { + mFileOutputStream.close(); + mStreamOpenend = false; + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/android/ConductorsPhone/app/src/main/res/layout/activity_main.xml b/android/ConductorsPhone/app/src/main/res/layout/activity_main.xml index 3c2d36b..c454833 100644 --- a/android/ConductorsPhone/app/src/main/res/layout/activity_main.xml +++ b/android/ConductorsPhone/app/src/main/res/layout/activity_main.xml @@ -1,23 +1,38 @@ - + android:orientation="vertical" + tools:context="de.tonifetzer.conductorswatch.MainActivity" + android:gravity="center"> + android:textSize="100sp" + android:textStyle="bold" + android:layout_centerHorizontal="true"/> - + + + + + diff --git a/android/ConductorsPhone/app/src/main/res/raw/metronom.flac b/android/ConductorsPhone/app/src/main/res/raw/metronom.flac new file mode 100644 index 0000000..fb19be0 Binary files /dev/null and b/android/ConductorsPhone/app/src/main/res/raw/metronom.flac differ diff --git a/android/ConductorsPhone/app/src/main/res/raw/metronom2.wav b/android/ConductorsPhone/app/src/main/res/raw/metronom2.wav new file mode 100644 index 0000000..9e1c88a Binary files /dev/null and b/android/ConductorsPhone/app/src/main/res/raw/metronom2.wav differ diff --git a/android/ConductorsPhone/app/src/main/res/raw/metronom3.wav b/android/ConductorsPhone/app/src/main/res/raw/metronom3.wav new file mode 100644 index 0000000..ff84b9c Binary files /dev/null and b/android/ConductorsPhone/app/src/main/res/raw/metronom3.wav differ diff --git a/android/ConductorsPhone/app/src/main/res/values/styles.xml b/android/ConductorsPhone/app/src/main/res/values/styles.xml index 5885930..0eb88fe 100644 --- a/android/ConductorsPhone/app/src/main/res/values/styles.xml +++ b/android/ConductorsPhone/app/src/main/res/values/styles.xml @@ -1,7 +1,7 @@ -