Skip to content

Commit f28a08c

Browse files
committed
Merge branch 'master' into DROID-03-PrepareGithubCI
2 parents ba51a89 + e1134e4 commit f28a08c

File tree

21 files changed

+570
-146
lines changed

21 files changed

+570
-146
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,4 @@ lint/generated/
8181
lint/outputs/
8282
lint/tmp/
8383
# lint/reports/
84+
app/src/main/res/values/api_keys.xml

.idea/misc.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,34 @@
55

66
This is being done as a hobby, and for experimenting, so probably there might be some flaws; As an example, the vendor ID of Arduino is hardcoded to only work with Arduino devices, but this is my use case and please feel free to change it to match your needs.
77

8+
# Build and Run
9+
The app is available for free on Google Play Store (Arduino USB Terminal).
10+
Otherwise, you can clone the project and run it locally.
11+
Please read the **Sentry Reports** part on this page before running the project to avoid build failures.
12+
13+
## Terminal
14+
A Simple terminal page which does what it is supposed to do interacting with an Arduino manually through the USB cable.
15+
16+
## Joystick
17+
The Joystick is removed for the first release.
18+
19+
## Tests
20+
Under Construction
21+
22+
## Sentry Reports
23+
The project uses Sentry for the crash reports, if this is not needed, you can remove the following line in `AndroidManifest.xml`:
24+
`<meta-data android:name="io.sentry.dsn" android:value="@string/sentry_dsn" />`
25+
But if it is needed, you need to [create a Sentry dsn value](https://docs.sentry.io/platforms/android/) to put under the following path:
26+
`app/src/main/res/values/api_keys.xml`
27+
The file contents might look like similar to this:
28+
`<?xml version="1.0" encoding="utf-8"?>
29+
<resources>
30+
<string name="sentry_dsn" translatable="false">YOUR_SENTRY_SPECIFIC_VALUE</string>
31+
</resources>`
32+
33+
834
### Knows Issues
9-
On Android 5.1.1, the Arduino serial output cannot be shown. (It is said that an Android internal bug is the issue!)
35+
_Still unknown! :)
1036

1137
Suggestions and PRs are welcome! :)
1238

app/build.gradle

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
apply plugin: 'com.android.application'
22
apply plugin: 'kotlin-android'
33
apply plugin: 'kotlin-android-extensions'
4+
apply plugin: 'koin'
5+
6+
repositories {
7+
jcenter()
8+
}
49

510
android {
611
compileSdkVersion 29
7-
buildToolsVersion "29.0.2"
12+
buildToolsVersion "29.0.3"
813

914
compileOptions {
1015
sourceCompatibility JavaVersion.VERSION_1_8
@@ -13,10 +18,10 @@ android {
1318

1419
defaultConfig {
1520
applicationId "org.kabiri.android.usbterminal"
16-
minSdkVersion 22
21+
minSdkVersion 23
1722
targetSdkVersion 29
18-
versionCode 1
19-
versionName "1.0"
23+
versionCode 6
24+
versionName "0.6.0"
2025

2126
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
2227
}
@@ -31,11 +36,26 @@ android {
3136
}
3237

3338
dependencies {
39+
3440
implementation fileTree(dir: 'libs', include: ['*.jar'])
3541
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
3642
implementation 'androidx.appcompat:appcompat:1.1.0'
3743
implementation 'androidx.core:core-ktx:1.2.0'
3844
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
45+
46+
// Sentry Tracking
47+
implementation 'io.sentry:sentry-android-core:2.0.2'
48+
49+
// Koin for Android - ViewModel features
50+
implementation "org.koin:koin-android-viewmodel:$koin_version"
51+
52+
// Coroutines
53+
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
54+
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
55+
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0'
56+
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'
57+
// testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.0-RC2'
58+
3959
testImplementation 'junit:junit:4.12'
4060
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
4161
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

app/src/main/AndroidManifest.xml

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,22 @@
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
33
package="org.kabiri.android.usbterminal">
44

5+
<uses-feature android:name="android.hardware.usb.host" />
6+
<uses-permission android:name="android.permission.INTERNET" />
7+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
8+
59
<application
10+
android:name=".MainApplication"
611
android:allowBackup="true"
12+
android:fullBackupContent="true"
713
android:icon="@mipmap/ic_launcher"
814
android:label="@string/app_name"
915
android:roundIcon="@mipmap/ic_launcher_round"
1016
android:supportsRtl="true"
1117
android:theme="@style/AppTheme">
18+
19+
<meta-data android:name="io.sentry.dsn" android:value="@string/sentry_dsn" />
20+
1221
<activity android:name=".MainActivity">
1322
<intent-filter>
1423
<action android:name="android.intent.action.MAIN" />
@@ -19,11 +28,10 @@
1928
<action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" />
2029
</intent-filter>
2130

22-
<meta-data android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"
23-
android:resource="@xml/accessory_filter"/>
31+
<meta-data
32+
android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"
33+
android:resource="@xml/accessory_filter" />
2434
</activity>
2535
</application>
2636

27-
<uses-feature android:name="android.hardware.usb.host" />
28-
2937
</manifest>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.kabiri.android.usbterminal
2+
3+
/**
4+
* Created by Ali Kabiri on 13.04.20.
5+
*/
6+
class Constants {
7+
companion object {
8+
const val ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION"
9+
}
10+
}

app/src/main/java/org/kabiri/android/usbterminal/MainActivity.kt

Lines changed: 28 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,164 +1,63 @@
11
package org.kabiri.android.usbterminal
22

3-
import android.app.PendingIntent
4-
import android.content.BroadcastReceiver
5-
import android.content.Context
6-
import android.content.Intent
7-
import android.content.IntentFilter
8-
import android.hardware.usb.UsbDevice
9-
import android.hardware.usb.UsbDeviceConnection
10-
import android.hardware.usb.UsbManager
11-
import android.os.Build
123
import android.os.Bundle
4+
import android.text.SpannableString
5+
import android.text.method.ScrollingMovementMethod
136
import android.util.Log
147
import android.view.Menu
158
import android.view.MenuInflater
169
import android.view.MenuItem
17-
import android.widget.Toast
1810
import androidx.appcompat.app.AppCompatActivity
19-
import com.felhr.usbserial.UsbSerialDevice
20-
import com.felhr.usbserial.UsbSerialInterface
11+
import androidx.lifecycle.Observer
12+
import io.sentry.core.Sentry
2113
import kotlinx.android.synthetic.main.activity_main.*
22-
import java.io.UnsupportedEncodingException
23-
import java.nio.charset.Charset
14+
import org.kabiri.android.usbterminal.viewmodel.MainActivityViewModel
15+
import org.koin.android.viewmodel.ext.android.viewModel
16+
2417

2518
class MainActivity : AppCompatActivity() {
2619

2720
companion object {
2821
private const val TAG = "MainActivity"
29-
private const val ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION"
3022
}
3123

32-
private lateinit var usbManager: UsbManager
33-
private lateinit var connection: UsbDeviceConnection
34-
private lateinit var serialPort: UsbSerialDevice
35-
private lateinit var usbReceiver: BroadcastReceiver
24+
private val viewModel: MainActivityViewModel by viewModel()
3625

3726
override fun onCreate(savedInstanceState: Bundle?) {
3827
super.onCreate(savedInstanceState)
3928
setContentView(R.layout.activity_main)
4029

41-
usbManager = getSystemService(Context.USB_SERVICE) as UsbManager
42-
usbReceiver = object : BroadcastReceiver() {
43-
44-
override fun onReceive(context: Context?, intent: Intent?) {
45-
when (intent?.action) {
46-
ACTION_USB_PERMISSION -> {
47-
synchronized(this) {
48-
val device: UsbDevice? =
49-
intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)
30+
// make the text view scrollable:
31+
tvOutput.movementMethod = ScrollingMovementMethod()
5032

51-
if (intent.getBooleanExtra(
52-
UsbManager.EXTRA_PERMISSION_GRANTED,
53-
false
54-
)
55-
) {
56-
tvOutput.append("\nPermission granted for ${device?.manufacturerName}")
57-
device?.apply {
58-
// setup the device communication.
59-
connection = usbManager.openDevice(device)
60-
serialPort = UsbSerialDevice
61-
.createUsbSerialDevice(device, connection)
62-
if (::serialPort.isInitialized) serialPort.let {
63-
if (it.open()) {
64-
// set connection params.
65-
it.setBaudRate(9600)
66-
it.setDataBits(UsbSerialInterface.DATA_BITS_8)
67-
it.setStopBits(UsbSerialInterface.STOP_BITS_1)
68-
it.setParity(UsbSerialInterface.PARITY_NONE)
69-
it.setFlowControl(UsbSerialInterface.FLOW_CONTROL_OFF)
70-
it.read { message ->
71-
// check if the Android version is not 5.1.1 Lollipop
72-
// before printing the message into output.
73-
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP_MR1) {
74-
Log.e(
75-
TAG,
76-
"Lollipop 5.1.1 is not supported to show the serial messages from the Arduino."
77-
)
78-
} else {
79-
message?.let {
80-
try {
81-
val encoded =
82-
String(
83-
message,
84-
Charset.defaultCharset()
85-
)
86-
tvOutput.append(encoded)
87-
} catch (e: UnsupportedEncodingException) {
88-
e.printStackTrace()
89-
tvOutput
90-
.append("\n${e.localizedMessage}")
91-
} catch (e: Exception) {
92-
Toast.makeText(
93-
this@MainActivity,
94-
e.localizedMessage,
95-
Toast.LENGTH_SHORT
96-
).show()
97-
}
98-
}
99-
}
100-
}
101-
tvOutput.append("\nSerial Connection Opened")
102-
} else {
103-
tvOutput.append("\nPort not opened")
104-
}
105-
} else {
106-
tvOutput.append("\nSerial Port was null")
107-
}
33+
// open the device and port when the permission is granted by user.
34+
viewModel.getGrantedDevice().observe(this, Observer { device ->
35+
viewModel.openDeviceAndPort(device)
36+
})
10837

109-
}
110-
} else {
111-
tvOutput.append("\npermission denied for device $device")
112-
}
113-
}
114-
}
115-
UsbManager.ACTION_USB_DEVICE_ATTACHED -> tvOutput.append("\nDevice attached")
116-
UsbManager.ACTION_USB_DEVICE_DETACHED -> tvOutput.append("\nDevice detached")
117-
}
118-
}
119-
}
38+
viewModel.getLiveOutput().observe(this, Observer {
39+
val spannable = SpannableString(it.text)
40+
spannable.setSpan(
41+
it.getAppearance(this),
42+
0,
43+
it.text.length,
44+
SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE)
45+
tvOutput.append(it.text)
46+
})
12047

48+
// send the command to device when the button is clicked.
12149
btEnter.setOnClickListener {
12250
val input = etInput.text.toString()
123-
try {
124-
if (::serialPort.isInitialized && input.isNotBlank()) {
125-
serialPort.write(input.toByteArray())
126-
tvOutput.append("\n") // this is because the answer might be sent in more than one part.
127-
etInput.setText("") // clear the terminal input.
128-
} else tvOutput.append("\nSerialPortNotOpened")
129-
} catch (e: Exception) {
130-
tvOutput.append("\n${e.localizedMessage}")
131-
}
51+
if (viewModel.serialWrite(input))
52+
etInput.setText("") // clear the terminal input.
53+
else Log.e(TAG, "The message was not sent to Arduino")
13254
}
133-
13455
}
13556

13657
override fun onOptionsItemSelected(item: MenuItem): Boolean {
13758
return when (item.itemId) {
13859
R.id.actionConnect -> {
139-
140-
val usbDevices = usbManager.deviceList
141-
if (usbDevices.isNotEmpty()) {
142-
for (device in usbDevices) {
143-
val deviceVID = device.value.vendorId
144-
if (deviceVID == 0x2341) { // Arduino vendor ID
145-
val permissionIntent = PendingIntent.getBroadcast(
146-
this,
147-
0,
148-
Intent(ACTION_USB_PERMISSION),
149-
0
150-
)
151-
val filter = IntentFilter(ACTION_USB_PERMISSION)
152-
registerReceiver(usbReceiver, filter) // register the broadcast receiver
153-
usbManager.requestPermission(device.value, permissionIntent)
154-
} else {
155-
tvOutput.append("\nArduino Device not found")
156-
connection.close()
157-
}
158-
}
159-
} else {
160-
tvOutput.append("\nNo USB devices are attached")
161-
}
60+
viewModel.askForConnectionPermission()
16261
true
16362
}
16463
else -> false
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.kabiri.android.usbterminal
2+
3+
import android.app.Application
4+
import org.kabiri.android.usbterminal.koin.appModule
5+
import org.koin.android.ext.koin.androidContext
6+
import org.koin.android.ext.koin.androidLogger
7+
import org.koin.core.context.startKoin
8+
import org.koin.core.logger.Level
9+
10+
/**
11+
* Created by Ali Kabiri on 13.04.20.
12+
*/
13+
class MainApplication: Application() {
14+
15+
override fun onCreate() {
16+
super.onCreate()
17+
18+
// start Koin context.
19+
startKoin{
20+
androidContext(this@MainApplication)
21+
androidLogger(Level.DEBUG)
22+
modules(appModule)
23+
}
24+
}
25+
}

0 commit comments

Comments
 (0)