Skip to content

Commit

Permalink
add USB support
Browse files Browse the repository at this point in the history
  • Loading branch information
teamclouday committed May 28, 2021
1 parent 0d7bded commit 8babc46
Show file tree
Hide file tree
Showing 19 changed files with 763 additions and 51 deletions.
14 changes: 12 additions & 2 deletions Android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ Android Microphone Project (Android Application folder)

### Structure

Three major threads:
Four major threads:
* UI thread
* Bluetooth client thread
* USB tcp client thread
* Audio recorder thread

------
Expand All @@ -27,6 +28,13 @@ Three major threads:
* transfer stored audio data
* cancel connection

#### USB Thread

* check if USB tethering is enabled
* connect to server based on input address
* transfer stored audio data
* cancel connection

#### Audio Thread

* check for permission
Expand All @@ -40,4 +48,6 @@ Three major threads:

This is the first Android application I write in Kotlin. The bluetooth part is basically the same as the Java applications I wrote before. However, Kotlin appears to be cleaner and shorter in length. A notable difference is that Kotlin uses `coroutines` to replace `AsyncTask` in Java, which I think is better. Another difference is that Kotlin is strict with `null` values and provide many ways to check them.

A drawback of this application is that it won't be able to run in background. So when connected, your application will force the screen on and will close connection whenever the app is put into background. A possible fix is to create [Background Services](https://developer.android.com/training/run-background-service/create-service) instead of threads I'm currently using. However, that requires much efforts in learning, and the communication with main activity is especially complex. I will leave it like that.
A drawback of this application is that it won't be able to run in background. So when connected, your application will force the screen on and will close connection whenever the app is put into background. A possible fix is to create [Background Services](https://developer.android.com/training/run-background-service/create-service) instead of threads I'm currently using. However, that requires much efforts in learning, and the communication with main activity is especially complex. I will leave it like that.

USB communication is achieved by enabling USB tethering. In this case, the network through PC can be detected by Android. By establishing a TCP socket, the device can communicate with PC app through USB.
4 changes: 2 additions & 2 deletions Android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ android {
applicationId "com.example.microphone"
minSdkVersion 19
targetSdkVersion 30
versionCode 2
versionName "1.1"
versionCode 3
versionName "1.2"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down
4 changes: 2 additions & 2 deletions Android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />

<application
android:allowBackup="true"
android:icon="@drawable/icon"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.Microphone">
<activity android:name=".MainActivity"
android:screenOrientation="portrait">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,6 @@ class BluetoothHelper(private val mActivity: MainActivity, private val mGlobalDa
// check if current socket is valid and connected
fun isSocketValid() : Boolean
{
return !(mSocket == null || mSocket?.isConnected == false)
return mSocket?.isConnected == true
}
}
195 changes: 181 additions & 14 deletions Android/app/src/main/java/com/example/microphone/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package com.example.microphone

import android.content.DialogInterface
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.text.InputType
import android.util.Log
import android.view.View
import android.view.WindowManager
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SwitchCompat
import kotlinx.coroutines.*
import java.lang.Exception
Expand All @@ -26,7 +30,10 @@ class GlobalState(
var isBluetoothStarted : Boolean,
var bluetoothShouldStop : Boolean,
var isAudioStarted : Boolean,
var audioShouldStop : Boolean
var audioShouldStop : Boolean,
var isUSBStarted : Boolean,
var usbShouldStop : Boolean,
var isUSBAddressSet : Boolean
)

// global data structure
Expand Down Expand Up @@ -69,13 +76,18 @@ class MainActivity : AppCompatActivity()
private val mUIScope = CoroutineScope(Dispatchers.Main)
private var mJobBluetooth : Job? = null
private var mJobAudio : Job? = null
private var mJobUSB : Job? = null

private lateinit var mLogTextView : TextView

private var helperBluetooth : BluetoothHelper? = null
private var helperAudio : AudioHelper? = null
private var helperUSB : USBHelper? = null

private val mGlobalState = GlobalState(
false,
false,
false,
false,
false,
false,
Expand All @@ -86,6 +98,7 @@ class MainActivity : AppCompatActivity()

private var threadBth : Thread? = null
private var threadAio : Thread? = null
private var threadUSB : Thread? = null

override fun onCreate(savedInstanceState: Bundle?)
{
Expand All @@ -99,41 +112,53 @@ class MainActivity : AppCompatActivity()

override fun onStop() {
super.onStop()
mGlobalState.bluetoothShouldStop = true
mGlobalState.audioShouldStop = true
threadBth?.join()
threadAio?.join()
helperBluetooth?.clean()
helperAudio?.clean()
clean()
}

override fun onResume() {
super.onResume()
findViewById<Button>(R.id.bth_connect).setText(R.string.turn_on_bth)
findViewById<Button>(R.id.usb_connect).setText(R.string.turn_on_usb)
findViewById<SwitchCompat>(R.id.audio_switch).isChecked = false
mGlobalState.isBluetoothStarted = false
mGlobalState.isAudioStarted = false
mGlobalState.isUSBStarted = false
}

override fun onPause() {
super.onPause()
mLogTextView.text = "" // clear log messages on pause
clean()
}

fun clean()
{
mGlobalState.bluetoothShouldStop = true
mGlobalState.audioShouldStop = true
mGlobalState.usbShouldStop = true
threadBth?.join()
threadAio?.join()
threadUSB?.join()
helperBluetooth?.clean()
helperAudio?.clean()
helperUSB?.clean()
if(mJobUSB?.isActive == true) mJobUSB?.cancel()
if(mJobBluetooth?.isActive == true) mJobBluetooth?.cancel()
if(mJobAudio?.isActive == true) mJobAudio?.cancel()
}

// onclick for bluetooth button
fun onButtonBluetooth(view : View)
{
if(!mGlobalState.isBluetoothStarted)
{
if(isConnected())
{
showToastMessage("Already connected")
return
}
val activity = this
if(mJobBluetooth?.isActive == true) return
this.showToastMessage("Starting bluetooth...")
Log.d(mLogTag, "onButtonBluetooth [start]")
mGlobalData.reset() // reset global data to store most recent audio
// launch a coroutine to prepare bluetooth helper object and start thread
Expand All @@ -150,8 +175,13 @@ class MainActivity : AppCompatActivity()
{
activity.addLogMessage("Error: " + e.message)
}
cancel()
null
}
withContext(Dispatchers.Main)
{
activity.showToastMessage("Starting bluetooth...")
}
// try to connect
if(helperBluetooth?.connect() == true)
{
Expand Down Expand Up @@ -182,17 +212,17 @@ class MainActivity : AppCompatActivity()
Thread.sleep(1)
}
}
threadBth?.start()

// update UI if success start
if(helperBluetooth != null && threadBth?.isAlive == true)
if(helperBluetooth?.isSocketValid() == true)
{
withContext(Dispatchers.Main)
{
(view as Button).setText(R.string.turn_off_bth)
}
mGlobalState.isBluetoothStarted = true
}

threadBth?.start()
}
}
}
Expand All @@ -210,12 +240,120 @@ class MainActivity : AppCompatActivity()
}
helperBluetooth?.clean()
helperBluetooth = null
threadBth = null
(view as Button).setText(R.string.turn_on_bth)
mGlobalState.isBluetoothStarted = false
}
}

// onclick for USB button
fun onButtonUSB(view : View)
{
if(!mGlobalState.isUSBStarted)
{
if(isConnected())
{
showToastMessage("Already connected")
return
}
val activity = this
if(mJobUSB?.isActive == true) return
Log.d(mLogTag, "onButtonUSB [start]")
mGlobalData.reset() // reset global data to store most recent audio
// launch a coroutine to prepare bluetooth helper object and start thread
mJobUSB = mUIScope.launch {
withContext(Dispatchers.Default)
{
mGlobalState.usbShouldStop = false
helperUSB?.clean()
// create object
helperUSB = try {
USBHelper(activity, mGlobalData)
} catch (e : IllegalArgumentException){
withContext(Dispatchers.Main)
{
activity.addLogMessage("Error: " + e.message)
}
cancel()
null
}
// get target IP address
withContext(Dispatchers.Main)
{
getIpAddress()
}
while(!mGlobalState.usbShouldStop && !mGlobalState.isUSBAddressSet)
{
try {
delay(200)
} catch (e : CancellationException) {break}
}
withContext(Dispatchers.Main)
{
activity.showToastMessage("Starting USB client...")
}
// try to connect
if(helperUSB?.connect() == true)
{
withContext(Dispatchers.Main)
{
activity.showToastMessage("Device connected")
activity.addLogMessage("Connected Device Information\n${helperUSB?.getConnectedDeviceInfo()}")
}
Log.d(mLogTag, "onButtonUSB [connect]")
}
// define and start the thread
threadUSB = Thread{
while (!mGlobalState.usbShouldStop) {
if (helperUSB?.isSocketValid() == true) // check if socket is disconnected
helperUSB?.sendData()
else {
// if not valid, disconnect the device
runOnUiThread {
activity.addLogMessage("Device disconnected")
mGlobalState.isUSBStarted = false
helperUSB?.clean()
helperUSB = null
threadUSB = null
(view as Button).setText(R.string.turn_on_usb)
}
break
}
Thread.sleep(1)
}
}
// update UI if success start
if(helperUSB?.isSocketValid() == true)
{
withContext(Dispatchers.Main)
{
(view as Button).setText(R.string.turn_off_usb)
}
mGlobalState.isUSBStarted = true
}

threadUSB?.start()
}
}
}
else
{
mGlobalState.usbShouldStop = true
this.showToastMessage("Stopping USB client...")
Log.d(mLogTag, "onButtonUSB [stop]")
ignore { threadUSB?.join(1000) }
threadUSB = null
if(helperUSB?.disconnect() == true)
{
this.showToastMessage("Device disconnected")
this.addLogMessage("Device disconnected successfully")
}
helperUSB?.clean()
helperUSB = null
(view as Button).setText(R.string.turn_on_usb)
mGlobalState.isUSBStarted = false
}
}

// on change for microphone switch
// basically similar to bluetooth function
fun onSwitchMic(view : View)
Expand Down Expand Up @@ -295,14 +433,43 @@ class MainActivity : AppCompatActivity()
}

// helper function to show toast message
fun showToastMessage(message : String)
private fun showToastMessage(message : String)
{
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}

// helper function to append log message to textview
fun addLogMessage(message : String)
private fun addLogMessage(message : String)
{
mLogTextView.append(message + "\n")
}

private fun isConnected() : Boolean
{
return mGlobalState.isUSBStarted || mGlobalState.isBluetoothStarted
}

private fun getIpAddress()
{
mGlobalState.isUSBAddressSet = false
val builder = AlertDialog.Builder(this)
builder.setTitle("PC IP Address")
val input = EditText(this)
input.setText("192.168.")
input.inputType = InputType.TYPE_CLASS_PHONE
builder.setView(input)
builder.setPositiveButton("OK"
) { dialog, which ->
if(helperUSB?.setAddress(input.text.toString()) != true)
addLogMessage("Invalid address: ${input.text}")
mGlobalState.isUSBAddressSet = true
}
builder.setOnCancelListener {
mGlobalState.isUSBAddressSet = true
}
builder.setOnDismissListener {
mGlobalState.isUSBAddressSet = true
}
builder.show()
}
}
Loading

0 comments on commit 8babc46

Please sign in to comment.