Skip to content

Commit

Permalink
Major project overhaul
Browse files Browse the repository at this point in the history
  • Loading branch information
cortinico committed Nov 2, 2024
1 parent a3734b3 commit 03bd559
Show file tree
Hide file tree
Showing 33 changed files with 394 additions and 419 deletions.
37 changes: 17 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ After setting up the Gradle dependency, you will be able to access two main clas

First, you need to **find a Myo** with a bluetooth scan.

** ⚠️ Please note that you need to request the user the ACCESS_COARSE_LOCATION permission. If not, the scan will be empty ⚠️ **
** ⚠️ Please note that you need to request the user the ACCESS_FINE_LOCATION permission. If not, the scan will be empty ⚠️ **

To start a bluetooth scan, you can use the `startScan()` method:

Expand Down Expand Up @@ -109,10 +109,10 @@ myMyo.statusObservable()
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
when (it) {
MyoStatus.CONNECTED -> { ... }
MyoStatus.CONNECTING -> { ... }
MyoStatus.READY -> { ... }
else -> { ... } // DISCONNECTED
MyoStatus.CONNECTED -> { /* ... */ }
MyoStatus.CONNECTING -> { /* ... */ }
MyoStatus.READY -> { /* ... */ }
else -> { /* ... */ } // DISCONNECTED
}
}
```
Expand Down Expand Up @@ -188,7 +188,7 @@ myMyo.keepAlive = false
You can find the test app (Myo Emg Visualizer) inside the `app` module.

<p align="center">
<img src="https://i.imgur.com/lcAbyJD.png" width="30%">
<img alt="test-app" src="https://i.imgur.com/lcAbyJD.png" width="30%">
</p>

This app allows you to:
Expand All @@ -210,44 +210,41 @@ Some technical features are:

**Searching for a Myo**

<img src="https://i.imgur.com/ShZP4w5.gif" width="30%"/>
<img alt="searching-for-a-myo" src="https://i.imgur.com/ShZP4w5.gif" width="30%"/>

**Starting the Streaming**

<img src="https://i.imgur.com/iqvkQfr.gif" width="30%"/>
<img alt="start-streaming" src="https://i.imgur.com/iqvkQfr.gif" width="30%"/>

**Exporting to CSV**

<img src="https://i.imgur.com/4UXIas9.gif" width="30%"/>
<img alt="exporting-the-csv" src="https://i.imgur.com/4UXIas9.gif" width="30%"/>


## Building/Testing ⚙️

### CircleCI [![CircleCI](https://circleci.com/gh/cortinico/myonnaise.svg?style=svg)](https://circleci.com/gh/cortinico/myonnaise)
### CircleCI

[![CircleCI](https://circleci.com/gh/cortinico/myonnaise.svg?style=svg)](https://circleci.com/gh/cortinico/myonnaise)

This projects is built with [**Circle CI 2.0**](https://circleci.com/gh/cortinico/myonnaise/). The CI environment takes care of building the library .AAR, the example app and to run the **JUnit** tests. Test and lint reports are exposes in the **artifacts** section at the end of every build.

### Codecov [![codecov](https://codecov.io/gh/cortinico/myonnaise/branch/master/graph/badge.svg)](https://codecov.io/gh/cortinico/myonnaise)
### Codecov

[![codecov](https://codecov.io/gh/cortinico/myonnaise/branch/master/graph/badge.svg)](https://codecov.io/gh/cortinico/myonnaise)

Circle CI is responsible of uploading Jacoco reports to [Codecov](https://codecov.io/gh/cortinico/myonnaise). When opening a Pull Request, Codecov will post a report of the diff of the test coverage.

Please **don't ignore it**! PR with new features and **without** are likely to be discarded 😕

### Building locally

Before building, make sure you have the following **updated components** from the Android SDK:

* tools
* platform-tools
* build-tools-28.0.1
* android-28

Then just clone the repo locally and build the .AAR with the following command:

```bash
git clone git@github.com:cortinico/myonnaise.git
cd myonnaise/
./gradlew app:assemble
./gradlew build
```
The assembled .AAR (library) will be inside the **myonnaise/build/outputs/aar** folder.
The assembled .APK (application) will be inside the **app/build/outputs/apk/debug** folder.
Expand All @@ -273,4 +270,4 @@ Make sure your tests are all green ✅ locally before submitting PRs.

## License 📄

This project is licensed under the MIT License - see the [License](License) file for details
This project is licensed under the MIT License - see the [License](LICENSE) file for details
9 changes: 3 additions & 6 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,22 @@
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />

<uses-feature android:name="android.hardware.bluetooth_le"
android:required="true" />

<application
android:name=".MyoApplication"
android:hardwareAccelerated="true"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:supportsRtl="true"
tools:ignore="AllowBackup,GoogleAppIndexingWarning">
tools:ignore="GoogleAppIndexingWarning">

<activity
android:name="it.ncorti.emgvisualizer.ui.IntroActivity">
android:name="it.ncorti.emgvisualizer.ui.IntroActivity"
android:exported="true">
</activity>

<activity
Expand Down
132 changes: 72 additions & 60 deletions app/src/main/java/it/ncorti/emgvisualizer/ui/IntroActivity.kt
Original file line number Diff line number Diff line change
@@ -1,81 +1,81 @@
package it.ncorti.emgvisualizer.ui

import android.Manifest
import android.Manifest.permission.ACCESS_FINE_LOCATION
import android.Manifest.permission.BLUETOOTH_ADMIN
import android.Manifest.permission.BLUETOOTH_CONNECT
import android.Manifest.permission.BLUETOOTH_SCAN
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Bundle
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.github.paolorotolo.appintro.AppIntro
import com.github.paolorotolo.appintro.AppIntroFragment
import com.github.paolorotolo.appintro.model.SliderPage
import com.github.appintro.AppIntro
import com.github.appintro.AppIntroFragment
import com.github.appintro.model.SliderPage
import it.ncorti.emgvisualizer.R

private const val PREFS_GLOBAL = "global"
private const val KEY_COMPLETED_ONBOARDING = "completed_onboarding"
private const val REQUEST_LOCATION_CODE = 1
private const val VIBRATE_INTENSITY = 30
private const val VIBRATE_INTENSITY = 30L

class IntroActivity : AppIntro() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val backgroundColor =
ContextCompat.getColor(this, R.color.primaryColor)

val page0 = SliderPage()
page0.title = getString(R.string.onboarding_title_0)
page0.description = getString(R.string.onboarding_description_0)
page0.imageDrawable = R.drawable.onboarding_0
page0.bgColor = backgroundColor
addSlide(AppIntroFragment.newInstance(page0))

val page1 = SliderPage()
page1.title = getString(R.string.scan)
page1.description = getString(R.string.onboarding_description_1)
page1.imageDrawable = R.drawable.onboarding_1
page1.bgColor = backgroundColor
addSlide(AppIntroFragment.newInstance(page1))

val page2 = SliderPage()
page2.title = getString(R.string.control)
page2.description = getString(R.string.onboarding_description_2)
page2.imageDrawable = R.drawable.onboarding_2
page2.bgColor = backgroundColor
addSlide(AppIntroFragment.newInstance(page2))

val page3 = SliderPage()
page3.title = getString(R.string.graph)
page3.description = getString(R.string.onboarding_description_3)
page3.imageDrawable = R.drawable.onboarding_3
page3.bgColor = backgroundColor
addSlide(AppIntroFragment.newInstance(page3))

val page4 = SliderPage()
page4.title = getString(R.string.export)
page4.description = getString(R.string.onboarding_description_4)
page4.imageDrawable = R.drawable.onboarding_4
page4.bgColor = backgroundColor
addSlide(AppIntroFragment.newInstance(page4))
addSlide(AppIntroFragment.createInstance(SliderPage().apply {
title = getString(R.string.onboarding_title_0)
description = getString(R.string.onboarding_description_0)
imageDrawable = R.drawable.onboarding_0
backgroundColorRes = R.color.primaryColor
}))

addSlide(AppIntroFragment.createInstance(SliderPage().apply {
title = getString(R.string.scan)
description = getString(R.string.onboarding_description_1)
imageDrawable = R.drawable.onboarding_1
backgroundColorRes = R.color.primaryColor
}))

addSlide(AppIntroFragment.createInstance(SliderPage().apply {
title = getString(R.string.control)
description = getString(R.string.onboarding_description_2)
imageDrawable = R.drawable.onboarding_2
backgroundColorRes = R.color.primaryColor
}))

addSlide(AppIntroFragment.createInstance(SliderPage().apply {
title = getString(R.string.graph)
description = getString(R.string.onboarding_description_3)
imageDrawable = R.drawable.onboarding_3
backgroundColorRes = R.color.primaryColor
}))

addSlide(AppIntroFragment.createInstance(SliderPage().apply {
title = getString(R.string.export)
description = getString(R.string.onboarding_description_4)
imageDrawable = R.drawable.onboarding_4
backgroundColorRes = R.color.primaryColor
}))

setBarColor(ContextCompat.getColor(this, R.color.primaryDarkColor))
setSeparatorColor(ContextCompat.getColor(this, R.color.primaryLightColor))
showSkipButton(false)
isProgressButtonEnabled = true
setVibrate(true)
setVibrateIntensity(VIBRATE_INTENSITY)
isSkipButtonEnabled = false
isVibrate = true
vibrateDuration = VIBRATE_INTENSITY
isButtonsEnabled = true
}

override fun onSkipPressed(currentFragment: Fragment) {
override fun onSkipPressed(currentFragment: Fragment?) {
super.onSkipPressed(currentFragment)
saveOnBoardingCompleted()
requestPermission()
}

override fun onDonePressed(currentFragment: Fragment) {
override fun onDonePressed(currentFragment: Fragment?) {
super.onDonePressed(currentFragment)
saveOnBoardingCompleted()
requestPermission()
Expand All @@ -88,28 +88,40 @@ class IntroActivity : AppIntro() {
}

private fun requestPermission() {
val hasPermission = (
ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
)
val hasPermission =
(ContextCompat.checkSelfPermission(this, ACCESS_FINE_LOCATION) == PERMISSION_GRANTED) &&
(ContextCompat.checkSelfPermission(
this,
BLUETOOTH_SCAN
) == PERMISSION_GRANTED) &&
(ContextCompat.checkSelfPermission(
this,
BLUETOOTH_ADMIN
) == PERMISSION_GRANTED) &&
(ContextCompat.checkSelfPermission(
this,
BLUETOOTH_CONNECT
) == PERMISSION_GRANTED)
if (hasPermission) {
startMainActivity()
} else {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION),
arrayOf(ACCESS_FINE_LOCATION, BLUETOOTH_SCAN, BLUETOOTH_ADMIN, BLUETOOTH_CONNECT),
REQUEST_LOCATION_CODE
)
}
}

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
REQUEST_LOCATION_CODE -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (grantResults.isNotEmpty() && grantResults[0] == PERMISSION_GRANTED) {
startMainActivity()
} else {
Toast.makeText(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ interface ControlDeviceContract {

fun showDeviceInformation(name: String?, address: String)

fun showConnectionProgress()

fun hideConnectionProgress()

fun showConnected()

fun showDisconnected()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,6 @@ class ControlDeviceFragment : BaseFragment<ControlDeviceContract.Presenter>(), C
binding.deviceAddress.text = address
}

override fun showConnectionProgress() {
binding.progressConnect.animate().alpha(1.0f)
}

override fun hideConnectionProgress() {
binding.progressConnect.animate().alpha(0.0f)
}

override fun showConnecting() {
binding.deviceStatus.text = getString(R.string.connecting)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

package it.ncorti.emgvisualizer.ui.control

import android.annotation.SuppressLint
import com.ncorti.myonnaise.CommandList
import com.ncorti.myonnaise.MYO_MAX_FREQUENCY
import com.ncorti.myonnaise.MyoControlStatus
Expand All @@ -12,6 +13,7 @@ import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import it.ncorti.emgvisualizer.dagger.DeviceManager

@SuppressLint("MissingPermission")
class ControlDevicePresenter(
override val view: ControlDeviceContract.View,
private val myonnaise: Myonnaise,
Expand Down Expand Up @@ -46,18 +48,15 @@ class ControlDevicePresenter(
.subscribe {
when (it) {
MyoStatus.CONNECTED -> {
view.hideConnectionProgress()
view.showConnected()
}
MyoStatus.CONNECTING -> {
view.showConnectionProgress()
view.showConnecting()
}
MyoStatus.READY -> {
view.enableControlPanel()
}
else -> {
view.hideConnectionProgress()
view.showDisconnected()
view.disableControlPanel()
}
Expand Down
Loading

0 comments on commit 03bd559

Please sign in to comment.