diff --git a/Android/README.md b/Android/README.md
index 13bea52..301424f 100644
--- a/Android/README.md
+++ b/Android/README.md
@@ -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
------
@@ -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
@@ -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.
\ No newline at end of file
+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.
\ No newline at end of file
diff --git a/Android/app/build.gradle b/Android/app/build.gradle
index 18a97b6..901633b 100644
--- a/Android/app/build.gradle
+++ b/Android/app/build.gradle
@@ -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"
}
diff --git a/Android/app/src/main/AndroidManifest.xml b/Android/app/src/main/AndroidManifest.xml
index bb3013d..dab343a 100644
--- a/Android/app/src/main/AndroidManifest.xml
+++ b/Android/app/src/main/AndroidManifest.xml
@@ -4,6 +4,7 @@
+
-
+
diff --git a/Android/app/src/main/java/com/example/microphone/BluetoothHelper.kt b/Android/app/src/main/java/com/example/microphone/BluetoothHelper.kt
index 7d0f94b..a122e7c 100644
--- a/Android/app/src/main/java/com/example/microphone/BluetoothHelper.kt
+++ b/Android/app/src/main/java/com/example/microphone/BluetoothHelper.kt
@@ -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
}
}
\ No newline at end of file
diff --git a/Android/app/src/main/java/com/example/microphone/MainActivity.kt b/Android/app/src/main/java/com/example/microphone/MainActivity.kt
index 08f5dca..f84d8fc 100644
--- a/Android/app/src/main/java/com/example/microphone/MainActivity.kt
+++ b/Android/app/src/main/java/com/example/microphone/MainActivity.kt
@@ -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
@@ -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
@@ -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,
@@ -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?)
{
@@ -99,31 +112,39 @@ 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(R.id.bth_connect).setText(R.string.turn_on_bth)
+ findViewById(R.id.usb_connect).setText(R.string.turn_on_usb)
findViewById(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
@@ -131,9 +152,13 @@ class MainActivity : AppCompatActivity()
{
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
@@ -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)
{
@@ -182,10 +212,8 @@ 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)
{
@@ -193,6 +221,8 @@ class MainActivity : AppCompatActivity()
}
mGlobalState.isBluetoothStarted = true
}
+
+ threadBth?.start()
}
}
}
@@ -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)
@@ -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()
+ }
}
\ No newline at end of file
diff --git a/Android/app/src/main/java/com/example/microphone/USBHelper.kt b/Android/app/src/main/java/com/example/microphone/USBHelper.kt
new file mode 100644
index 0000000..64474e5
--- /dev/null
+++ b/Android/app/src/main/java/com/example/microphone/USBHelper.kt
@@ -0,0 +1,151 @@
+package com.example.microphone
+
+import android.util.Log
+import android.util.Patterns
+import java.io.DataInputStream
+import java.io.DataOutputStream
+import java.io.EOFException
+import java.io.IOException
+import java.net.NetworkInterface
+import java.net.Socket
+
+
+// enable connection through USB tethering
+class USBHelper(private val mActivity: MainActivity, private val mGlobalData : GlobalData)
+{
+ private val mLogTag : String = "AndroidMicUSB"
+
+ private val MAX_WAIT_TIME = 1500 // timeout
+ private val DEVICE_CHECK_DATA : Int = 123456
+ private val DEVICE_CHECK_EXPECTED : Int = 654321
+ private val PORT = 55555
+
+ private var mSocket : Socket? = null
+ private var mAddress : String = ""
+
+ // init and check for USB tethering
+ init
+ {
+ // reference: https://stackoverflow.com/questions/43478586/checking-tethering-usb-bluetooth-is-active
+ // reference: https://airtower.wordpress.com/2010/07/29/getting-network-interface-information-in-java/
+ val ifs = NetworkInterface.getNetworkInterfaces()
+ var tetheringEnabled = false
+ while(ifs.hasMoreElements())
+ {
+ val iface = ifs.nextElement()
+ Log.d(mLogTag, "init checking iface = " + iface.name)
+ if(iface.name == "rndis0" || iface.name == "ap0")
+ {
+ tetheringEnabled = true
+ break
+ }
+ }
+ require(tetheringEnabled){"USB tethering is not enabled"}
+
+ // can also programmatically enable tethering according to this
+ // but no idea how to stop tethering after program exit
+ // reference: https://stackoverflow.com/questions/3436280/start-stop-built-in-wi-fi-usb-tethering-from-code
+ // I prefer that the users can only enable it themselves
+ }
+
+ // connect to target device
+ fun connect() : Boolean
+ {
+ // create socket and connect
+ mSocket = try {
+ Socket(mAddress, PORT)
+ } catch (e : IOException) {
+ Log.d(mLogTag, "connect [Socket]: ${e.message}")
+ null
+ } ?: return false
+ mSocket?.soTimeout = MAX_WAIT_TIME
+ // validate with server
+ if(!validate())
+ {
+ mSocket?.close()
+ mSocket = null
+ return false
+ }
+ return true
+ }
+
+ // send data through socket
+ fun sendData()
+ {
+ if(mSocket == null) return
+ val nextData = mGlobalData.getData() ?: return
+ try {
+ val stream = mSocket?.outputStream
+ stream?.write(nextData)
+ stream?.flush()
+ // Log.d(mLogTag, "[sendData] data sent (${nextData.size} bytes)")
+ } catch (e : IOException)
+ {
+ Log.d(mLogTag, "${e.message}")
+ Thread.sleep(4)
+ disconnect()
+ }
+ }
+
+ // disconnect from target device
+ fun disconnect() : Boolean
+ {
+ if(mSocket != null)
+ {
+ ignore { mSocket?.close() }
+ mSocket = null
+ }
+ return true
+ }
+
+ // clean object
+ fun clean()
+ {
+ disconnect()
+ }
+
+ // validate connection with the server
+ private fun validate() : Boolean
+ {
+ if(!isSocketValid()) return false
+ var isValid = false
+ try {
+ val streamOut = DataOutputStream(mSocket?.outputStream)
+ streamOut.writeInt(DEVICE_CHECK_DATA)
+ streamOut.flush()
+ val streamIn = DataInputStream(mSocket?.inputStream)
+ if(streamIn.readInt() == DEVICE_CHECK_EXPECTED)
+ isValid = true
+ } catch (e : EOFException) {
+ Log.d(mLogTag, "validate error: ${e.message}")
+ ignore { mSocket?.close() }
+ mSocket = null
+ } catch (e : IOException) {
+ Log.d(mLogTag, "validate error: ${e.message}")
+ ignore { mSocket?.close() }
+ mSocket = null
+ }
+ return isValid
+ }
+
+ // set and validate IP address
+ fun setAddress(address : String) : Boolean
+ {
+ mAddress = address
+ Log.d(mLogTag, "setAddress ${address}")
+ return Patterns.IP_ADDRESS.matcher(address).matches()
+ }
+
+ // get connected device information
+ fun getConnectedDeviceInfo() : String
+ {
+ if(mSocket == null) return ""
+ return "[Device Address] ${mSocket?.remoteSocketAddress}"
+ }
+
+ // check if current socket is valid and connected
+ fun isSocketValid() : Boolean
+ {
+ return mSocket?.isConnected == true
+ }
+}
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout-land/activity_main.xml b/Android/app/src/main/res/layout-land/activity_main.xml
new file mode 100644
index 0000000..8cd7043
--- /dev/null
+++ b/Android/app/src/main/res/layout-land/activity_main.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/activity_main.xml b/Android/app/src/main/res/layout/activity_main.xml
index a58824f..b26db67 100644
--- a/Android/app/src/main/res/layout/activity_main.xml
+++ b/Android/app/src/main/res/layout/activity_main.xml
@@ -15,7 +15,7 @@
android:layout_marginBottom="16dp"
android:onClick="onButtonBluetooth"
android:text="@string/turn_on_bth"
- app:layout_constraintBottom_toTopOf="@+id/audio_switch"
+ app:layout_constraintBottom_toTopOf="@+id/usb_connect"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.497"
app:layout_constraintStart_toStartOf="parent" />
@@ -36,12 +36,13 @@
app:layout_constraintStart_toStartOf="parent" />
+ android:textColor="@color/black" />
+
+
\ No newline at end of file
diff --git a/Android/app/src/main/res/values/strings.xml b/Android/app/src/main/res/values/strings.xml
index 379fe1e..b458c71 100644
--- a/Android/app/src/main/res/values/strings.xml
+++ b/Android/app/src/main/res/values/strings.xml
@@ -2,5 +2,8 @@
Mic
Connect Bluetooth
Disconnect Bluetooth
+ Connect USB
+ Disconnect USB
Record Audio
+
\ No newline at end of file
diff --git a/Assets/p1.png b/Assets/p1.png
index de6e841..5107830 100644
Binary files a/Assets/p1.png and b/Assets/p1.png differ
diff --git a/Assets/p2.jpg b/Assets/p2.jpg
index 568e1d7..43be44f 100644
Binary files a/Assets/p2.jpg and b/Assets/p2.jpg differ
diff --git a/Assets/p3.jpg b/Assets/p3.jpg
new file mode 100644
index 0000000..4a75fb2
Binary files /dev/null and b/Assets/p3.jpg differ
diff --git a/README.md b/README.md
index b2ea820..1e9ec3e 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ You have a Windows desktop PC but do not have a mic (or have a Sony bluetooth he
------
### Requirements
-* Android phone with bluetooth
+* Android phone with bluetooth (or USB tethering)
* Windows PC with bluetooth
* PC and phone are paired once
* Installed [Virtual Audio Cable (VAC)](https://vac.muzychenko.net/en/) on Windows, will hear "trial" voice if your driver is in trial mode
@@ -18,15 +18,29 @@ You have a Windows desktop PC but do not have a mic (or have a Sony bluetooth he
### How to use
+_Bluetooth Method_:
+
1. Run Windows side application first, click `connect` to start server
-2. Next launch Android side application, click `connect` and enable `microphone`
+2. Next launch Android side application, click `CONNECT BLUETOOTH` and enable `microphone`
3. Select audio speaker from drop down list to the one that VAC (or VB) created
4. Use the corresponding microphone created by VAC (or VB)
+_USB Method_:
+1. Connect phone with PC by cable
+2. Enable USB tethering on Android
+3. (Optional) Change connected network priority so that PC will not consume all your phone's network
+4. Launch Windows app and select your server IP address assigned by the USB network (find in `Settings`->`Network & Internet`->`Ethernet`->Click->`Properties`->`IPv4 address`)
+5. On Android side, click `CONNECT USB` and enter the same IP you selected on Windows side
+6. Enable microphone and select audio device following the same steps in bluetooth method
+
That's all!
+------
+
+
+
------
### Future Feature Plan
@@ -34,7 +48,7 @@ That's all!
- [x] Windows app can minimize to system tray
- [x] Volume control on Windows side
- [x] Audio visualization on Windows side
-- [ ] USB serial port connection support
+- [x] USB serial port connection support
- [ ] Make Android side able to run in background
- [ ] Show notification when mic is in use on Android side
@@ -51,6 +65,10 @@ Pre-built installers can be found [here](https://github.com/teamclouday/AndroidM
-### Android Side
+### Android Side (Portrait)
+
+
+
+### Android Side (Landscape)
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/Windows/AndroidMic/AndroidMic.csproj b/Windows/AndroidMic/AndroidMic.csproj
index f45c8ae..ec680ee 100644
--- a/Windows/AndroidMic/AndroidMic.csproj
+++ b/Windows/AndroidMic/AndroidMic.csproj
@@ -30,7 +30,7 @@
AndroidMic
Teamclouday
1
- 1.1.0.%2a
+ 1.2.0.%2a
false
true
true
@@ -128,6 +128,7 @@
MSBuild:Compile
Designer
+
MSBuild:Compile
diff --git a/Windows/AndroidMic/BluetoothHelper.cs b/Windows/AndroidMic/BluetoothHelper.cs
index ad6fab9..c5396b7 100644
--- a/Windows/AndroidMic/BluetoothHelper.cs
+++ b/Windows/AndroidMic/BluetoothHelper.cs
@@ -32,14 +32,20 @@ class BluetoothHelper
private BluetoothEndPoint mTargetDeviceID = null;
public BthStatus Status { get; private set; } = BthStatus.DEFAULT;
+
+ private void SetStatus(BthStatus value)
+ {
+ Status = value;
+ }
+
private bool isConnectionAllowed = false;
private Thread mProcessThread = null;
private readonly MainWindow mMainWindow;
private readonly AudioData mGlobalData;
- public BluetoothHelper(MainWindow mainWindow, AudioData globalData)
- {
+ public BluetoothHelper(MainWindow mainWindow, AudioData globalData)
+ {
mMainWindow = mainWindow;
mGlobalData = globalData;
}
@@ -55,7 +61,7 @@ public void StartServer()
};
mListener.Start();
}
- Status = BthStatus.LISTENING;
+ SetStatus(BthStatus.LISTENING);
Debug.WriteLine("[BluetoothHelper] server started");
AddLog("Service started listening...");
Accept();
@@ -78,7 +84,7 @@ public void StopServer()
mListener.Stop();
mListener = null;
}
- Status = BthStatus.DEFAULT;
+ SetStatus(BthStatus.DEFAULT);
AddLog("Service stopped");
Debug.WriteLine("[BluetoothHelper] server stopped");
}
@@ -96,7 +102,7 @@ private void AcceptCallback(IAsyncResult result)
{
if(!isConnectionAllowed)
{
- Status = BthStatus.DEFAULT;
+ SetStatus(BthStatus.DEFAULT);
return;
}
if(result.IsCompleted)
@@ -113,6 +119,9 @@ private void AcceptCallback(IAsyncResult result)
Debug.WriteLine("[BluetoothHelper] cliented detected, checking...");
if (TestClient(client))
{
+ // close current client
+ client.Dispose();
+ client.Close();
Debug.WriteLine("[BluetoothHelper] client valid");
// stop alive thread
if (mProcessThread != null && mProcessThread.IsAlive)
@@ -157,6 +166,9 @@ private void AcceptCallback(IAsyncResult result)
}
else
{
+ // close current client
+ client.Dispose();
+ client.Close();
Debug.WriteLine("[BluetoothHelper] client invalid");
if (client != null) client.Dispose();
Accept();
@@ -164,6 +176,7 @@ private void AcceptCallback(IAsyncResult result)
}
}
+ // check if client is validate
private bool TestClient(BluetoothClient client)
{
if (client == null) return false;
@@ -180,11 +193,7 @@ private bool TestClient(BluetoothClient client)
if (!stream.CanRead || !stream.CanWrite) return false;
// check received integer
if (stream.Read(receivedPack, 0, receivedPack.Length) == 0) return false;
- else if (DecodeInt(receivedPack) != DEVICE_CHECK_EXPECTED)
- {
- int tmp = DecodeInt(receivedPack);
- return false;
- }
+ else if (DecodeInt(receivedPack) != DEVICE_CHECK_EXPECTED) return false;
// send back integer for verification
stream.Write(sentPack, 0, sentPack.Length);
// save valid target client ID
@@ -197,16 +206,13 @@ private bool TestClient(BluetoothClient client)
Debug.WriteLine("[BluetoothHelper] TestClient error: " + e.Message);
return false;
}
- // close current client
- client.Dispose();
- client.Close();
return true;
}
// receive audio data
private void Process()
{
- Status = BthStatus.CONNECTED;
+ SetStatus(BthStatus.CONNECTED);
while(isConnectionAllowed && IsClientValid())
{
try
@@ -240,7 +246,7 @@ private void Disconnect()
{
if(mClient != null)
{
- Status = BthStatus.DEFAULT;
+ SetStatus(BthStatus.DEFAULT);
mClient.Dispose();
mClient = null;
}
diff --git a/Windows/AndroidMic/MainWindow.xaml b/Windows/AndroidMic/MainWindow.xaml
index b16a361..c2717f8 100644
--- a/Windows/AndroidMic/MainWindow.xaml
+++ b/Windows/AndroidMic/MainWindow.xaml
@@ -38,6 +38,9 @@
+
+
diff --git a/Windows/AndroidMic/MainWindow.xaml.cs b/Windows/AndroidMic/MainWindow.xaml.cs
index 3b65f04..21cdf5b 100644
--- a/Windows/AndroidMic/MainWindow.xaml.cs
+++ b/Windows/AndroidMic/MainWindow.xaml.cs
@@ -3,9 +3,7 @@
using System.ComponentModel;
using System.Collections.Generic;
using System.Windows;
-using System.Windows.Media;
using System.Windows.Input;
-using System.Windows.Shapes;
using System.Windows.Controls;
using System.Windows.Documents;
@@ -39,6 +37,7 @@ public partial class MainWindow : Window
private readonly AudioData mGlobalData = new AudioData();
private readonly BluetoothHelper mHelperBluetooth;
private readonly AudioHelper mHelperAudio;
+ private readonly USBHelper mHelperUSB;
public WaveDisplay mWaveformDisplay;
private readonly System.Windows.Forms.NotifyIcon notifyIcon = new System.Windows.Forms.NotifyIcon();
@@ -48,6 +47,9 @@ public MainWindow()
// init objects
mHelperBluetooth = new BluetoothHelper(this, mGlobalData);
mHelperAudio = new AudioHelper(this, mGlobalData);
+ mHelperUSB = new USBHelper(this, mGlobalData);
+ // setup USB ips
+ SetupUSBList();
// setup audio
SetupAudioList();
mHelperAudio.Start();
@@ -95,6 +97,7 @@ private void MainWindow_Closing(object sender, CancelEventArgs e)
{
mHelperBluetooth.StopServer();
mHelperAudio.Stop();
+ mHelperUSB.Clean();
notifyIcon.Dispose();
}
@@ -104,7 +107,11 @@ private void ConnectButton_Click(object sender, RoutedEventArgs e)
Button button = (Button)sender;
if(mHelperBluetooth.Status == BthStatus.DEFAULT)
{
- if(!BluetoothHelper.CheckBluetooth())
+ if(IsConnected())
+ {
+ AddLogMessage("You have already connected");
+ }
+ else if(!BluetoothHelper.CheckBluetooth())
{
MessageBox.Show("Bluetooth not enabled\nPlease enable it and try again", "AndroidMic Bluetooth", MessageBoxButton.OK);
}
@@ -138,6 +145,13 @@ private void AudioDeviceList_DropDownClosed(object sender, EventArgs e)
if (mHelperAudio.RefreshAudioDevices()) SetupAudioList();
}
+ // drop down closed for combobox of USB server IP addreses
+ private void USBIP_DropDownClosed(object sender, EventArgs e)
+ {
+ mHelperUSB.StartServer(USBIPAddresses.SelectedIndex - 1);
+ if (mHelperUSB.RefreshIpAdress()) SetupUSBList();
+ }
+
// volume slider change callback
private void VolumeSlider_PropertyChange(object sender, RoutedPropertyChangedEventArgs e)
{
@@ -166,6 +180,19 @@ private void SetupAudioList()
}
}
+ // set up USB IP list
+ private void SetupUSBList()
+ {
+ USBIPAddresses.Items.Clear();
+ USBIPAddresses.Items.Add("Disabled");
+ USBIPAddresses.SelectedIndex = 0;
+ string[] addresses = mHelperUSB.IPAddresses;
+ foreach(string address in addresses)
+ {
+ USBIPAddresses.Items.Add(address);
+ }
+ }
+
// help function to append log message to text block
public void AddLogMessage(string message)
{
@@ -184,5 +211,10 @@ public void RefreshWaveform(short valPos, short valNeg)
if(WindowState != WindowState.Minimized)
mWaveformDisplay.AddData(valPos, valNeg);
}
+
+ public bool IsConnected()
+ {
+ return (mHelperBluetooth.Status == BthStatus.CONNECTED) || (mHelperUSB.Status == USBStatus.CONNECTED);
+ }
}
}
diff --git a/Windows/AndroidMic/USBHelper.cs b/Windows/AndroidMic/USBHelper.cs
new file mode 100644
index 0000000..e7f62ad
--- /dev/null
+++ b/Windows/AndroidMic/USBHelper.cs
@@ -0,0 +1,232 @@
+using System;
+using System.Net;
+using System.Net.Sockets;
+using System.Windows;
+using System.Threading;
+using System.Diagnostics;
+using System.Collections.Generic;
+
+namespace AndroidMic
+{
+ enum USBStatus
+ {
+ DEFAULT,
+ LISTENING,
+ CONNECTED
+ }
+
+ // Reference: https://www.c-sharpcorner.com/article/socket-programming-in-C-Sharp/
+
+ // helper class to connect to device through USB tethering
+ class USBHelper
+ {
+ private readonly int MAX_WAIT_TIME = 1500;
+ private readonly int BUFFER_SIZE = 2048;
+ private readonly int PORT = 55555;
+ private readonly int DEVICE_CHECK_EXPECTED = 123456;
+ private readonly int DEVICE_CHECK_DATA = 654321;
+ private IPHostEntry mHost;
+ private int mSelectedAddressID;
+ private Socket mServer;
+ public string[] IPAddresses { get; private set; }
+
+ private bool isConnectionAllowed = false;
+ private Thread mThreadServer;
+
+ public USBStatus Status { get; private set; } = USBStatus.DEFAULT;
+
+ private readonly MainWindow mMainWindow;
+ private readonly AudioData mGlobalData;
+
+ public USBHelper(MainWindow mainWindow, AudioData globalData)
+ {
+ mMainWindow = mainWindow;
+ mGlobalData = globalData;
+ RefreshIpAdress();
+ }
+
+ // clean before application closes
+ public void Clean()
+ {
+ StopServer();
+ }
+
+ // start server and listen for connection
+ public void StartServer(int idx)
+ {
+ StopServer();
+ if (idx < 0)
+ {
+ Application.Current.Dispatcher.Invoke(new Action(() =>
+ {
+ mMainWindow.mWaveformDisplay.Reset();
+ }));
+ AddLog("Server stopped");
+ return; // idx < 0 means disabled server
+ }
+ isConnectionAllowed = true;
+ mSelectedAddressID = idx;
+ mThreadServer = new Thread(new ThreadStart(Process));
+ mThreadServer.Start();
+ }
+
+ // accept connection and process data
+ private void Process()
+ {
+ IPAddress ipAddress = IPAddress.Parse(IPAddresses[mSelectedAddressID]);
+ IPEndPoint endPoint = new IPEndPoint(ipAddress, PORT);
+ // first try to connect to client
+ Socket client;
+ Status = USBStatus.LISTENING;
+ AddLog("Server started (" + IPAddresses[mSelectedAddressID] + ")");
+ Debug.WriteLine("[USBHelper] server started");
+ try
+ {
+ mServer = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
+ mServer.Bind(endPoint);
+ mServer.Listen(5); // 5 requests at a time
+ do
+ {
+ client = mServer.Accept();
+ if (ValidateClient(client)) break;
+ client.Close();
+ client.Dispose();
+ } while (isConnectionAllowed);
+ } catch(SocketException e)
+ {
+ Debug.WriteLine("[USBHelper] error: " + e.Message);
+ return;
+ }
+ Status = USBStatus.CONNECTED;
+ Debug.WriteLine("[USBHelper] client connected");
+ AddLog("Device connected\nclient [Address]: " + client.LocalEndPoint);
+ // start processing
+ while(isConnectionAllowed && client.Connected)
+ {
+ try
+ {
+ byte[] buffer = new byte[BUFFER_SIZE];
+ int bufferSize = client.Receive(buffer, 0, BUFFER_SIZE, SocketFlags.None);
+ if (bufferSize == 0)
+ {
+ Thread.Sleep(5);
+ break;
+ }
+ else if (bufferSize < 0) break;
+ mGlobalData.AddData(buffer, bufferSize);
+ //Debug.WriteLine("[USBHelper] Process buffer received (" + bufferSize + " bytes)");
+ }
+ catch (SocketException e)
+ {
+ Debug.WriteLine("[USBHelper] Process error: " + e.Message);
+ break;
+ }
+ Thread.Sleep(1);
+ }
+ // after that clean socket
+ client.Close();
+ client.Dispose();
+ AddLog("Device disconnected");
+ Debug.WriteLine("[USBHelper] client disconnected");
+ Status = USBStatus.DEFAULT;
+ Application.Current.Dispatcher.Invoke(new Action(() =>
+ {
+ mMainWindow.mWaveformDisplay.Reset();
+ }));
+ }
+
+ // stop server
+ private void StopServer()
+ {
+ if (mServer != null)
+ {
+ mServer.Close();
+ mServer.Dispose();
+ mServer = null;
+ }
+ isConnectionAllowed = false;
+ if (mThreadServer != null && mThreadServer.IsAlive)
+ {
+ if (mThreadServer.Join(MAX_WAIT_TIME)) mThreadServer.Abort();
+ }
+ Debug.WriteLine("[USBHelper] server stopped");
+ }
+
+ // check if client is valid
+ private bool ValidateClient(Socket client)
+ {
+ if (client == null || !client.Connected) return false;
+ byte[] receivedPack = new byte[4];
+ byte[] sentPack = BluetoothHelper.EncodeInt(DEVICE_CHECK_DATA);
+ try
+ {
+ // client.ReceiveTimeout = MAX_WAIT_TIME;
+ // client.SendTimeout = MAX_WAIT_TIME;
+ // check received integer
+ int offset = 0;
+ do
+ {
+ int sizeReceived = client.Receive(receivedPack, offset, receivedPack.Length-offset, SocketFlags.None);
+ if (sizeReceived <= 0)
+ {
+ Debug.WriteLine("[USBHelper] Invalid client (size received: " + sizeReceived + ")");
+ return false;
+ }
+ offset += sizeReceived;
+ } while (offset < 4);
+
+ if (BluetoothHelper.DecodeInt(receivedPack) != DEVICE_CHECK_EXPECTED)
+ {
+ Debug.WriteLine("[USBHelper] Invalid client (expected: " + DEVICE_CHECK_EXPECTED + ", but get: " + BluetoothHelper.DecodeInt(receivedPack) + ")");
+ return false;
+ }
+ // send back integer for verification
+ client.Send(sentPack, sentPack.Length, SocketFlags.None);
+ }
+ catch (SocketException e)
+ {
+ Debug.WriteLine("[USBHelper] ValidateClient error: " + e.Message);
+ return false;
+ }
+ Debug.WriteLine("[USBHelper] Valid client");
+ return true;
+ }
+
+ // refresh IP adresses
+ public bool RefreshIpAdress()
+ {
+ bool changed = false;
+ mHost = Dns.GetHostEntry(Dns.GetHostName());
+ List addresses = new List();
+ foreach(var ip in mHost.AddressList)
+ {
+ if (ip.AddressFamily == AddressFamily.InterNetwork)
+ addresses.Add(ip.ToString());
+ }
+ if(IPAddresses == null || addresses.Count != IPAddresses.Length)
+ {
+ IPAddresses = new string[addresses.Count];
+ changed = true;
+ }
+ for (int i = 0; i < IPAddresses.Length; i++)
+ {
+ string address = addresses[i];
+ if (IPAddresses[i] != address)
+ {
+ changed = true;
+ IPAddresses[i] = address;
+ }
+ }
+ return changed;
+ }
+
+ // helper function to add log message
+ private void AddLog(string message)
+ {
+ Application.Current.Dispatcher.Invoke(new Action(() =>
+ {
+ mMainWindow.AddLogMessage("[USB]\n" + message + "\n");
+ }));
+ }
+ }
+}
diff --git a/Windows/README.md b/Windows/README.md
index c5b2ccd..4f533a2 100644
--- a/Windows/README.md
+++ b/Windows/README.md
@@ -6,9 +6,10 @@ Android Microphone Project (Windows Application folder)
### Structure
-Three major threads:
+Four major threads:
* UI thread
-* Bluetooth client thread
+* Bluetooth server thread
+* USB tcp server thread
* Audio recorder thread
------
@@ -28,6 +29,15 @@ Three major threads:
* cancel connection
* stop server
+#### USB Thread
+
+* start USB tcp server
+* select server address
+* establish connection
+* receive audio data
+* cancel connection
+* stop server
+
#### Audio Thread
* start wave out player
@@ -48,4 +58,4 @@ My method of displaying raw byte audio array in real time is:
2. Whenever new data is received from stream, run a while loop and check the maximum float (at least 0) and minimum float (at most 0) in current screen
3. If current screen is full, add current max and min `short` values to the wave display
-Another interesting thing is that because I already converted the `short` array to `byte` array based on Big Endian on Android side, I don't need to reverse bytes to get a `short` value from the stream.
\ No newline at end of file
+Another interesting thing is that since I already converted the `short` array to `byte` array based on Big Endian on Android side, I don't need to reverse bytes to get a `short` value from the stream.
\ No newline at end of file