diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..522dcb2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# Local configuration file +local.properties + +# Java class files +*.class + +# Built native files +*.o +*.so +obj/ + +# Built application files +*.apk +*.ap_ + +# Generated files +bin/ +gen/ + +# Gradle files +.gradle/ +build/ + +# IntelliJ Idea +.idea/ +*.iml + +# OS-specific files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Proguard folder +proguard/ + +# Log Files +*.log + +# EmpaLink/E4link lib +empalink*.aar +E4link*.aar + +gradlew* +gradle/* +docs diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..44b88b0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2015, Empatica Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Empatica nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..118aa70 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# EmpaLink Sample Project + +## Introduction + +This sample project gives you the boilerplate code you need to connect to an Empatica E4 device and start streaming data. + +The sample application implemented in the project has very simple functionalities: + +- It initializes the EmpaLink library with your API key. +- If the previous step is successful, it starts scanning for Empatica devices, till it finds one that can be used with the API key you inserted in the code. +- When such a device has been found, the app connects to the devices and streams data for 10 seconds, then it disconnects. + +## Setup + +- Clone / download this repository. +- Open the sample project in Android Studio. +- Make sure you have a valid API key. You can request one for your Empatica Connect account from our [Developer Area][1]. +- Edit `MainActivity.java` and assign your API key to the `EMPATICA_API_KEY` constant . +- Download the Android SDK from our [Developer Area][1]. +- Unzip the archive you've downloaded and copy the `.aar` file you'll find inside into the `libs` folder contained in the sample project. +- Build and run the project. +- If a device is in range and its light is blinking green, but the app doesn't connect, please check that the discovered device can be used with your API key. If the `allowed` parameter is always false, the device is not linked to your API key. Please check your [Developer Area][1]. + +If you need any additional information about the Empatica API for Android, please check the [official documentation][2]. + +[1]: https://www.empatica.com/connect/developer.php +[2]: http://developer.empatica.com diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..3350ab0 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,60 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 28 + buildToolsVersion '29.0.3' + defaultConfig { + applicationId 'com.empatica.empalinksample' + minSdkVersion 19 + targetSdkVersion 28 + versionCode 2323 + versionName "1.1" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + productFlavors { + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_6 + targetCompatibility JavaVersion.VERSION_1_6 + } +} + +//// DE-COMMENT THIS IF YOU HAVE ISSUES AT RUNTIME, CRASHING WITH java.lang.UnsatisfiedLinkError + +//android { +// +// splits { +// +// // Configures multiple APKs based on ABI. +// abi { +// +// // Enables building multiple APKs per ABI. +// enable true +// +// // By default all ABIs are included, so use reset() and include to specify that we only +// // want APKs for x86, armeabi-v7a, and mips. +// +// // Resets the list of ABIs that Gradle should create APKs for to none. +// reset() +// +// // Specifies a list of ABIs that Gradle should create APKs for. +// include 'armeabi-v7a', 'x86', 'x86_64' // de-comment this to test on emulator +// // include 'armeabi-v7a' +// +// // Specifies that we do not want to also generate a universal APK that includes all ABIs. +// universalApk false +// } +// } +//} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'com.empatica.empalink:E4link:1.0.0@aar' + implementation 'com.squareup.okhttp:okhttp:2.7.5' +} \ No newline at end of file diff --git a/app/libs/ b/app/libs/ new file mode 100644 index 0000000..e66b232 --- /dev/null +++ b/app/libs/ @@ -0,0 +1 @@ +COPY HERE THE EMPALINK LIBRARY \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..77a0560 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/andrea/Library/Android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8f4d68e --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/empatica/sample/MainActivity.java b/app/src/main/java/com/empatica/sample/MainActivity.java new file mode 100644 index 0000000..a859853 --- /dev/null +++ b/app/src/main/java/com/empatica/sample/MainActivity.java @@ -0,0 +1,379 @@ +package com.empatica.sample; + +import android.Manifest; +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.provider.Settings; +import android.support.annotation.NonNull; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; +import android.text.TextUtils; +import android.view.View; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +import com.empatica.empalink.ConnectionNotAllowedException; +import com.empatica.empalink.EmpaDeviceManager; +import com.empatica.empalink.EmpaticaDevice; +import com.empatica.empalink.config.EmpaSensorStatus; +import com.empatica.empalink.config.EmpaSensorType; +import com.empatica.empalink.config.EmpaStatus; +import com.empatica.empalink.delegate.EmpaDataDelegate; +import com.empatica.empalink.delegate.EmpaStatusDelegate; + + +public class MainActivity extends AppCompatActivity implements EmpaDataDelegate, EmpaStatusDelegate { + + private static final int REQUEST_ENABLE_BT = 1; + + private static final int REQUEST_PERMISSION_ACCESS_COARSE_LOCATION = 1; + + + private static final String EMPATICA_API_KEY = ""; // TODO insert your API Key here + + + private EmpaDeviceManager deviceManager = null; + + private TextView accel_xLabel; + + private TextView accel_yLabel; + + private TextView accel_zLabel; + + private TextView bvpLabel; + + private TextView edaLabel; + + private TextView ibiLabel; + + private TextView temperatureLabel; + + private TextView batteryLabel; + + private TextView statusLabel; + + private TextView deviceNameLabel; + + private LinearLayout dataCnt; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_main); + + // Initialize vars that reference UI components + statusLabel = (TextView) findViewById(R.id.status); + + dataCnt = (LinearLayout) findViewById(R.id.dataArea); + + accel_xLabel = (TextView) findViewById(R.id.accel_x); + + accel_yLabel = (TextView) findViewById(R.id.accel_y); + + accel_zLabel = (TextView) findViewById(R.id.accel_z); + + bvpLabel = (TextView) findViewById(R.id.bvp); + + edaLabel = (TextView) findViewById(R.id.eda); + + ibiLabel = (TextView) findViewById(R.id.ibi); + + temperatureLabel = (TextView) findViewById(R.id.temperature); + + batteryLabel = (TextView) findViewById(R.id.battery); + + deviceNameLabel = (TextView) findViewById(R.id.deviceName); + + + final Button disconnectButton = findViewById(R.id.disconnectButton); + + disconnectButton.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + + if (deviceManager != null) { + + deviceManager.disconnect(); + } + } + }); + + initEmpaticaDeviceManager(); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + switch (requestCode) { + case REQUEST_PERMISSION_ACCESS_COARSE_LOCATION: + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // Permission was granted, yay! + initEmpaticaDeviceManager(); + } else { + // Permission denied, boo! + final boolean needRationale = ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_COARSE_LOCATION); + new AlertDialog.Builder(this) + .setTitle("Permission required") + .setMessage("Without this permission bluetooth low energy devices cannot be found, allow it in order to connect to the device.") + .setPositiveButton("Retry", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // try again + if (needRationale) { + // the "never ask again" flash is not set, try again with permission request + initEmpaticaDeviceManager(); + } else { + // the "never ask again" flag is set so the permission requests is disabled, try open app settings to enable the permission + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", getPackageName(), null); + intent.setData(uri); + startActivity(intent); + } + } + }) + .setNegativeButton("Exit application", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // without permission exit is the only way + finish(); + } + }) + .show(); + } + break; + } + } + + private void initEmpaticaDeviceManager() { + // Android 6 (API level 23) now require ACCESS_COARSE_LOCATION permission to use BLE + if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, new String[] { Manifest.permission.ACCESS_COARSE_LOCATION }, REQUEST_PERMISSION_ACCESS_COARSE_LOCATION); + } else { + + if (TextUtils.isEmpty(EMPATICA_API_KEY)) { + new AlertDialog.Builder(this) + .setTitle("Warning") + .setMessage("Please insert your API KEY") + .setNegativeButton("Close", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // without permission exit is the only way + finish(); + } + }) + .show(); + return; + } + + // Create a new EmpaDeviceManager. MainActivity is both its data and status delegate. + deviceManager = new EmpaDeviceManager(getApplicationContext(), this, this); + + // Initialize the Device Manager using your API key. You need to have Internet access at this point. + deviceManager.authenticateWithAPIKey(EMPATICA_API_KEY); + } + } + + @Override + protected void onPause() { + super.onPause(); + if (deviceManager != null) { + deviceManager.stopScanning(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (deviceManager != null) { + deviceManager.cleanUp(); + } + } + + @Override + public void didDiscoverDevice(EmpaticaDevice bluetoothDevice, String deviceName, int rssi, boolean allowed) { + // Check if the discovered device can be used with your API key. If allowed is always false, + // the device is not linked with your API key. Please check your developer area at + // https://www.empatica.com/connect/developer.php + if (allowed) { + // Stop scanning. The first allowed device will do. + deviceManager.stopScanning(); + try { + // Connect to the device + deviceManager.connectDevice(bluetoothDevice); + updateLabel(deviceNameLabel, "To: " + deviceName); + } catch (ConnectionNotAllowedException e) { + // This should happen only if you try to connect when allowed == false. + Toast.makeText(MainActivity.this, "Sorry, you can't connect to this device", Toast.LENGTH_SHORT).show(); + } + } + } + + @Override + public void didFailedScanning(int errorCode) { + + } + + @Override + public void didRequestEnableBluetooth() { + // Request the user to enable Bluetooth + Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); + startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); + } + + @Override + public void bluetoothStateChanged() { + + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + // The user chose not to enable Bluetooth + if (requestCode == REQUEST_ENABLE_BT && resultCode == Activity.RESULT_CANCELED) { + // You should deal with this + return; + } + super.onActivityResult(requestCode, resultCode, data); + } + + @Override + public void didUpdateSensorStatus(@EmpaSensorStatus int status, EmpaSensorType type) { + + didUpdateOnWristStatus(status); + } + + @Override + public void didUpdateStatus(EmpaStatus status) { + // Update the UI + updateLabel(statusLabel, status.name()); + + // The device manager is ready for use + if (status == EmpaStatus.READY) { + updateLabel(statusLabel, status.name() + " - Turn on your device"); + // Start scanning + deviceManager.startScanning(); + // The device manager has established a connection + + hide(); + + } else if (status == EmpaStatus.CONNECTED) { + + show(); + // The device manager disconnected from a device + } else if (status == EmpaStatus.DISCONNECTED) { + + updateLabel(deviceNameLabel, ""); + + hide(); + } + } + + @Override + public void didReceiveAcceleration(int x, int y, int z, double timestamp) { + updateLabel(accel_xLabel, "" + x); + updateLabel(accel_yLabel, "" + y); + updateLabel(accel_zLabel, "" + z); + } + + @Override + public void didReceiveBVP(float bvp, double timestamp) { + updateLabel(bvpLabel, "" + bvp); + } + + @Override + public void didReceiveBatteryLevel(float battery, double timestamp) { + updateLabel(batteryLabel, String.format("%.0f %%", battery * 100)); + } + + @Override + public void didReceiveGSR(float gsr, double timestamp) { + updateLabel(edaLabel, "" + gsr); + } + + @Override + public void didReceiveIBI(float ibi, double timestamp) { + updateLabel(ibiLabel, "" + ibi); + } + + @Override + public void didReceiveTemperature(float temp, double timestamp) { + updateLabel(temperatureLabel, "" + temp); + } + + // Update a label with some text, making sure this is run in the UI thread + private void updateLabel(final TextView label, final String text) { + runOnUiThread(new Runnable() { + @Override + public void run() { + label.setText(text); + } + }); + } + + @Override + public void didReceiveTag(double timestamp) { + + } + + @Override + public void didEstablishConnection() { + + show(); + } + + @Override + public void didUpdateOnWristStatus(@EmpaSensorStatus final int status) { + + runOnUiThread(new Runnable() { + + @Override + public void run() { + + if (status == EmpaSensorStatus.ON_WRIST) { + + ((TextView) findViewById(R.id.wrist_status_label)).setText("ON WRIST"); + } + else { + + ((TextView) findViewById(R.id.wrist_status_label)).setText("NOT ON WRIST"); + } + } + }); + } + + void show() { + + runOnUiThread(new Runnable() { + + @Override + public void run() { + + dataCnt.setVisibility(View.VISIBLE); + } + }); + } + + void hide() { + + runOnUiThread(new Runnable() { + + @Override + public void run() { + + dataCnt.setVisibility(View.INVISIBLE); + } + }); + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..3593c54 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +