├── .github └── workflows │ └── release.yml ├── .gitignore ├── Android ├── .gitignore ├── app │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src │ │ ├── debug │ │ └── res │ │ │ ├── values │ │ │ └── strings.xml │ │ │ └── xml │ │ │ └── accessory_filter.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── ic_launcher-playstore.png │ │ ├── java │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── androidMic │ │ │ │ ├── AndroidMicApp.kt │ │ │ │ ├── AppModule.kt │ │ │ │ ├── Preferences.kt │ │ │ │ ├── domain │ │ │ │ ├── audio │ │ │ │ │ └── MicAudioManager.kt │ │ │ │ ├── service │ │ │ │ │ ├── Command.kt │ │ │ │ │ ├── ForegroundService.kt │ │ │ │ │ ├── Packets.kt │ │ │ │ │ └── ServiceUtil.kt │ │ │ │ └── streaming │ │ │ │ │ ├── AdbStreamer.kt │ │ │ │ │ ├── BluetoothStreamer.kt │ │ │ │ │ ├── MicStreamManager.kt │ │ │ │ │ ├── Streamer.kt │ │ │ │ │ ├── UdpStreamer.kt │ │ │ │ │ ├── UsbStreamer.kt │ │ │ │ │ └── WifiStreamer.kt │ │ │ │ ├── ui │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── MainViewModel.kt │ │ │ │ ├── components │ │ │ │ │ └── Components.kt │ │ │ │ ├── home │ │ │ │ │ ├── AppBar.kt │ │ │ │ │ ├── HomeScreen.kt │ │ │ │ │ ├── NavigationDrawer.kt │ │ │ │ │ └── dialog │ │ │ │ │ │ ├── BaseDialog.kt │ │ │ │ │ │ ├── audio.kt │ │ │ │ │ │ ├── ipPort.kt │ │ │ │ │ │ ├── mode.kt │ │ │ │ │ │ └── theme.kt │ │ │ │ ├── theme │ │ │ │ │ ├── Color.kt │ │ │ │ │ ├── Theme.kt │ │ │ │ │ └── Type.kt │ │ │ │ └── utils │ │ │ │ │ ├── PermissionHelper.kt │ │ │ │ │ ├── UiHelper.kt │ │ │ │ │ ├── ViewModelFactoryHelper.kt │ │ │ │ │ └── rememberWindowInfo.kt │ │ │ │ └── utils │ │ │ │ ├── PreferencesManager.kt │ │ │ │ └── Utils.kt │ │ ├── proto │ │ │ └── message.proto │ │ └── res │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_background.xml │ │ │ ├── drawable │ │ │ ├── ic_launcher_foreground.xml │ │ │ └── ic_launcher_monochrome.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── values-fr │ │ │ └── strings.xml │ │ │ ├── values │ │ │ ├── strings.xml │ │ │ └── themes.xml │ │ │ └── xml │ │ │ ├── accessory_filter.xml │ │ │ └── locales_config.xml │ │ ├── releaseProto │ │ └── res │ │ │ └── values │ │ │ └── strings.xml │ │ └── releaseTesting │ │ └── res │ │ └── values │ │ └── strings.xml ├── build.gradle.kts ├── gradle.properties ├── gradle │ ├── libs.versions.toml │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts ├── Assets ├── README.md ├── icon512.png ├── microphone-3404243_640.png ├── p1.png ├── p2.png ├── p3.png ├── p4.png ├── p5.png ├── sound_config1.png ├── sound_config2.png ├── sound_config3.png ├── sound_config4.png ├── sound_config5.png └── sound_config6.png ├── LICENSE ├── Project_Tree.txt ├── README.md └── RustApp ├── .cargo └── config.toml ├── .gitattributes ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── build.rs ├── i18n.toml ├── i18n ├── en │ └── android_mic.ftl └── fr │ └── android_mic.ftl ├── io.github.teamclouday.android-mic.json ├── justfile ├── res ├── icons │ ├── icon.svg │ └── refresh24.svg ├── linux │ ├── app_icon.svg │ ├── desktop_entry.desktop │ └── metainfo.xml ├── macos │ ├── Info.plist │ └── entitlements.plist └── windows │ └── app_icon.ico ├── rust-toolchain.toml └── src ├── audio ├── denoise.rs ├── mod.rs ├── player.rs ├── process.rs └── resampler.rs ├── config.rs ├── localize.rs ├── main.rs ├── proto └── message.proto ├── start_at_login.rs ├── streamer ├── adb_streamer.rs ├── message.rs ├── mod.rs ├── streamer_runner.rs ├── tcp_streamer.rs ├── udp_streamer.rs ├── usb │ ├── aoa.rs │ ├── frame.rs │ └── mod.rs └── usb_streamer.rs ├── ui ├── app.rs ├── icon.rs ├── message.rs ├── mod.rs ├── view.rs └── wave.rs └── utils.rs /.gitignore: -------------------------------------------------------------------------------- 1 | release/* 2 | *.zip 3 | .vscode 4 | .DS_Store -------------------------------------------------------------------------------- /Android/.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle files 2 | .gradle/ 3 | build/ 4 | 5 | # Local configuration file (sdk path, etc) 6 | local.properties 7 | 8 | # Log/OS Files 9 | *.log 10 | 11 | # Android Studio generated files and folders 12 | captures/ 13 | .externalNativeBuild/ 14 | .cxx/ 15 | *.apk 16 | output.json 17 | 18 | # IntelliJ 19 | *.iml 20 | .idea/ 21 | 22 | # Keystore files 23 | *.jks 24 | *.keystore 25 | 26 | # Google Services (e.g. APIs or Firebase) 27 | google-services.json 28 | 29 | # Android Profiling 30 | *.hprof 31 | 32 | .kotlin -------------------------------------------------------------------------------- /Android/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | src/androidTest/* 3 | src/test/* 4 | release/* -------------------------------------------------------------------------------- /Android/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.google.protobuf.gradle.* 2 | 3 | plugins { 4 | alias(libs.plugins.android.application) 5 | alias(libs.plugins.kotlin.android) 6 | alias(libs.plugins.compose.compiler) 7 | alias(libs.plugins.google.protobuf) 8 | } 9 | 10 | android { 11 | namespace = "com.example.androidMic" 12 | compileSdk = 35 13 | 14 | defaultConfig { 15 | applicationId = "com.example.androidMic" 16 | minSdk = 23 17 | targetSdk = 35 18 | versionCode = 9 19 | versionName = "2.1.5" 20 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 21 | 22 | vectorDrawables.useSupportLibrary = true 23 | androidResources { 24 | localeFilters += listOf("en", "fr") 25 | } 26 | 27 | } 28 | 29 | buildTypes { 30 | release { 31 | isMinifyEnabled = false 32 | proguardFiles( 33 | getDefaultProguardFile("proguard-android-optimize.txt"), 34 | "proguard-rules.pro" 35 | ) 36 | signingConfig = signingConfigs.getByName("debug") 37 | } 38 | 39 | debug { 40 | applicationIdSuffix = ".debug" 41 | } 42 | } 43 | 44 | compileOptions { 45 | sourceCompatibility = JavaVersion.VERSION_21 46 | targetCompatibility = JavaVersion.VERSION_21 47 | } 48 | 49 | kotlinOptions { 50 | jvmTarget = "21" 51 | } 52 | buildFeatures { 53 | prefab = true 54 | compose = true 55 | } 56 | 57 | lint { 58 | abortOnError = false 59 | checkReleaseBuilds = false 60 | } 61 | 62 | // packaging { 63 | // resources.excludes.add("google/protobuf/*.proto") 64 | // } 65 | 66 | sourceSets.getByName("main").resources.srcDir("src/main/proto") 67 | } 68 | 69 | protobuf { 70 | protoc { 71 | artifact = "com.google.protobuf:protoc:3.25.5" 72 | } 73 | 74 | generateProtoTasks { 75 | all().forEach { task -> 76 | task.builtins { 77 | id("java") { 78 | option("lite") 79 | } 80 | } 81 | } 82 | } 83 | } 84 | 85 | dependencies { 86 | // AndroidX Core 87 | implementation(libs.androidx.ktx) 88 | implementation(libs.androidx.viewmodel.compose) 89 | implementation(libs.runtime.ktx) 90 | implementation(libs.runtime.compose) 91 | implementation(libs.compose.activity) 92 | implementation(libs.datastore.preferences) 93 | 94 | 95 | val composeBom = platform(libs.compose.bom) 96 | 97 | // Compose 98 | implementation(composeBom) 99 | implementation(libs.compose.ui) 100 | implementation(libs.compose.material) 101 | implementation(libs.compose.material3) 102 | implementation(libs.compose.material.icons.extended) 103 | implementation(libs.compose.constraintlayout) 104 | 105 | // compose permission 106 | implementation(libs.accompanist.permissions) 107 | 108 | // Compose Debug 109 | implementation(libs.compose.ui.preview) 110 | debugImplementation(libs.androidx.ui.tooling) 111 | 112 | // Streaming 113 | implementation(libs.protobuf.java.lite) 114 | implementation(libs.protobuf.gradle.plugin) 115 | 116 | // unit test 117 | testImplementation(libs.test.junit.ktx) 118 | 119 | // integration test 120 | androidTestImplementation(composeBom) 121 | androidTestImplementation(libs.test.junit.ktx) 122 | androidTestImplementation(libs.kotlinx.coroutines.test) 123 | androidTestImplementation(libs.androidx.runner) 124 | } -------------------------------------------------------------------------------- /Android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /Android/app/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AndroidMic-Debug 3 | -------------------------------------------------------------------------------- /Android/app/src/debug/res/xml/accessory_filter.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /Android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 27 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 46 | 47 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Android/app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclouday/AndroidMic/0b02cce1250e75f1bb5b2499973722f2263849a1/Android/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/AndroidMicApp.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic 2 | 3 | import android.app.Application 4 | import android.content.ComponentName 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.ServiceConnection 8 | import android.os.IBinder 9 | import android.os.Messenger 10 | import android.util.Log 11 | import com.example.androidMic.domain.service.ForegroundService 12 | import com.example.androidMic.ui.MainActivity 13 | import kotlinx.coroutines.MainScope 14 | import kotlinx.coroutines.launch 15 | 16 | 17 | class AndroidMicApp : Application() { 18 | private val TAG = "AndroidMicApp" 19 | 20 | 21 | companion object { 22 | lateinit var appModule: AppModule 23 | var mService: Messenger? = null 24 | var mBound = false 25 | } 26 | 27 | private val scope = MainScope() 28 | override fun onCreate() { 29 | super.onCreate() 30 | 31 | appModule = AppModuleImpl(this) 32 | 33 | scope.launch { 34 | appModule.appPreferences.preload() 35 | } 36 | } 37 | 38 | private val mConnection = object : ServiceConnection { 39 | override fun onServiceConnected(name: ComponentName?, service: IBinder?) { 40 | Log.d(TAG, "onServiceConnected") 41 | mService = Messenger(service) 42 | mBound = true 43 | // notify current running activity that service is connected 44 | val notifyIntent = Intent(applicationContext, MainActivity::class.java).apply { 45 | action = Intent.ACTION_VIEW 46 | addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) 47 | putExtra("ForegroundServiceBound", true) 48 | } 49 | startActivity(notifyIntent) 50 | } 51 | 52 | override fun onServiceDisconnected(name: ComponentName?) { 53 | Log.d(TAG, "onServiceDisconnected") 54 | mService = null 55 | mBound = false 56 | } 57 | } 58 | 59 | // start and bind to service 60 | fun bindService() { 61 | val intent = Intent(this, ForegroundService::class.java) 62 | startService(intent) 63 | bindService(intent, mConnection, Context.BIND_AUTO_CREATE) 64 | } 65 | 66 | fun unBindService() { 67 | unbindService(mConnection) 68 | mService = null 69 | mBound = false 70 | } 71 | } -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic 2 | 3 | import android.content.Context 4 | import com.example.androidMic.ui.utils.UiHelper 5 | 6 | 7 | interface AppModule { 8 | val appPreferences: AppPreferences 9 | val uiHelper: UiHelper 10 | val context: Context 11 | 12 | } 13 | 14 | class AppModuleImpl( 15 | override val context: Context 16 | ) : AppModule { 17 | override val appPreferences: AppPreferences by lazy { 18 | AppPreferences(context) 19 | } 20 | override val uiHelper: UiHelper by lazy { 21 | UiHelper(context) 22 | } 23 | } -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/Preferences.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import androidx.annotation.RequiresApi 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.res.stringResource 8 | import com.example.androidMic.utils.PreferencesManager 9 | 10 | object DefaultStates { 11 | const val IP = "192.168." 12 | const val PORT = "55555" 13 | } 14 | 15 | /** 16 | * Rules: key should be upper case snake case 17 | * Ex: SAMPLE_RATE 18 | * The key should match the name in the app state 19 | */ 20 | class AppPreferences( 21 | context: Context 22 | ) : PreferencesManager(context, "settings") { 23 | val mode = enumPreference("mode", Mode.WIFI) 24 | 25 | val ip = stringPreference("ip", "192.168.") 26 | val port = stringPreference("port", "55555") 27 | 28 | 29 | val theme = enumPreference("theme", Themes.System) 30 | val dynamicColor = booleanPreference("dynamicColor", true) 31 | 32 | val sampleRate = enumPreference("sampleRate", SampleRates.S44100) 33 | val channelCount = enumPreference("channelCount", ChannelCount.Mono) 34 | val audioFormat = enumPreference("audioFormat", AudioFormat.I16) 35 | 36 | } 37 | 38 | enum class Mode { 39 | WIFI, UDP, USB, ADB 40 | } 41 | 42 | enum class Themes { 43 | System, 44 | Dark, 45 | Light 46 | } 47 | 48 | enum class Dialogs { 49 | IpPort, 50 | } 51 | 52 | // well, this can go crazy: https://github.com/audiojs/sample-rate 53 | enum class SampleRates(val value: Int) { 54 | S8000(8000), 55 | S11025(11025), 56 | S16000(16000), 57 | S22050(22050), 58 | S44100(44100), 59 | S48000(48000), 60 | S88200(88200), 61 | S96600(96600), 62 | S176400(176400), 63 | S192000(192000), 64 | S352800(352800), 65 | S384000(384000), 66 | } 67 | 68 | enum class AudioFormat(val value: Int, val description: String) { 69 | I8(android.media.AudioFormat.ENCODING_PCM_8BIT, "u8"), 70 | I16(android.media.AudioFormat.ENCODING_PCM_16BIT, "i16"), 71 | 72 | @RequiresApi(Build.VERSION_CODES.S) 73 | I24(android.media.AudioFormat.ENCODING_PCM_24BIT_PACKED, "i24"), 74 | 75 | @RequiresApi(Build.VERSION_CODES.S) 76 | I32(android.media.AudioFormat.ENCODING_PCM_32BIT, "i32"), 77 | F32(android.media.AudioFormat.ENCODING_PCM_FLOAT, "f32"); 78 | 79 | override fun toString(): String = description 80 | } 81 | 82 | 83 | enum class ChannelCount(val value: Int) { 84 | Mono(1), 85 | Stereo(2); 86 | 87 | @Composable 88 | fun getString(): String { 89 | 90 | return when (this) { 91 | Mono -> stringResource(R.string.mono) 92 | Stereo -> stringResource(R.string.stereo) 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/domain/audio/MicAudioManager.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.domain.audio 2 | 3 | import android.Manifest 4 | import android.content.Context 5 | import android.content.pm.PackageManager 6 | import android.media.AudioFormat 7 | import android.media.AudioRecord 8 | import android.media.MediaRecorder 9 | import android.util.Log 10 | import androidx.core.content.ContextCompat 11 | import com.example.androidMic.domain.service.AudioPacket 12 | import kotlinx.coroutines.CoroutineScope 13 | import kotlinx.coroutines.Job 14 | import kotlinx.coroutines.channels.awaitClose 15 | import kotlinx.coroutines.delay 16 | import kotlinx.coroutines.flow.Flow 17 | import kotlinx.coroutines.flow.channelFlow 18 | import kotlinx.coroutines.launch 19 | 20 | // manage microphone recording 21 | class MicAudioManager( 22 | ctx: Context, 23 | val scope: CoroutineScope, 24 | val sampleRate: Int, 25 | val audioFormat: Int, 26 | val channelCount: Int 27 | 28 | ) { 29 | private val TAG: String = "MicAM" 30 | 31 | companion object { 32 | const val RECORD_DELAY_MS = 100L 33 | } 34 | 35 | private val recorder: AudioRecord 36 | private val bufferSize: Int 37 | private val buffer: ByteArray 38 | private var streamJob: Job? = null 39 | 40 | init { 41 | // check microphone 42 | require(ctx.packageManager.hasSystemFeature(PackageManager.FEATURE_MICROPHONE)) { 43 | "Microphone is not detected on this device" 44 | } 45 | require( 46 | ContextCompat.checkSelfPermission( 47 | ctx, 48 | Manifest.permission.RECORD_AUDIO 49 | ) == PackageManager.PERMISSION_GRANTED 50 | ) { 51 | "Microphone recording is not permitted" 52 | } 53 | 54 | // get minimum buffer size 55 | val channelConfig = 56 | if (channelCount == 2) AudioFormat.CHANNEL_IN_STEREO else AudioFormat.CHANNEL_IN_MONO 57 | bufferSize = AudioRecord.getMinBufferSize( 58 | sampleRate, 59 | channelConfig, 60 | audioFormat, 61 | ) 62 | 63 | require(bufferSize != AudioRecord.ERROR && bufferSize != AudioRecord.ERROR_BAD_VALUE) { 64 | "Microphone buffer size ($bufferSize) is invalid\nAudio format is likely not supported" 65 | } 66 | 67 | // init recorder 68 | recorder = AudioRecord( 69 | MediaRecorder.AudioSource.MIC, 70 | sampleRate, 71 | channelConfig, 72 | audioFormat, 73 | bufferSize, 74 | ) 75 | 76 | // check if recorder is initialized 77 | require(recorder.state == AudioRecord.STATE_INITIALIZED) { 78 | "Microphone recording failed to initialize" 79 | } 80 | 81 | buffer = ByteArray(bufferSize) 82 | } 83 | 84 | // audio stream publisher 85 | fun audioStream(): Flow = channelFlow { 86 | // launch in scope so infinite loop will be canceled when scope exits 87 | streamJob = scope.launch { 88 | while (true) { 89 | if (recorder.state != AudioRecord.STATE_INITIALIZED || recorder.recordingState != AudioRecord.RECORDSTATE_RECORDING) { 90 | delay(RECORD_DELAY_MS) 91 | continue 92 | } 93 | val bytesRead = recorder.read(buffer, 0, buffer.size) 94 | 95 | if (bytesRead <= 0) { 96 | delay(RECORD_DELAY_MS) 97 | continue 98 | } 99 | 100 | // Log.d(TAG, "audioStream: $bytesRead bytes read") 101 | 102 | val packetBuffer = ByteArray(bytesRead) 103 | buffer.copyInto(packetBuffer, 0, 0, bytesRead) 104 | send( 105 | AudioPacket( 106 | buffer = packetBuffer, 107 | sampleRate = sampleRate, 108 | audioFormat = audioFormat, 109 | channelCount = channelCount 110 | ) 111 | ) 112 | } 113 | } 114 | 115 | awaitClose { 116 | streamJob?.cancel() 117 | } 118 | } 119 | 120 | // start recording 121 | fun start() { 122 | recorder.startRecording() 123 | Log.d(TAG, "start") 124 | } 125 | 126 | // stop recording 127 | fun stop() { 128 | recorder.stop() 129 | Log.d(TAG, "stop") 130 | } 131 | 132 | // shutdown manager 133 | // should not call any methods after calling 134 | fun shutdown() { 135 | recorder.stop() 136 | recorder.release() 137 | streamJob?.cancel() 138 | Log.d(TAG, "shutdown") 139 | } 140 | } -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/domain/service/Command.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.domain.service 2 | 3 | import android.os.Bundle 4 | import android.os.Message 5 | import com.example.androidMic.AudioFormat 6 | import com.example.androidMic.ChannelCount 7 | import com.example.androidMic.Mode 8 | import com.example.androidMic.SampleRates 9 | 10 | 11 | private const val ID_MSG: String = "ID_MSG" 12 | private const val ID_STATE: String = "ID_STATE" 13 | 14 | private const val ID_MODE: String = "ID_MODE" 15 | 16 | private const val ID_IP: String = "ID_IP" 17 | private const val ID_PORT: String = "ID_PORT" 18 | 19 | private const val ID_SAMPLE_RATE: String = "ID_SAMPLE_RATE" 20 | private const val ID_CHANNEL_COUNT: String = "ID_CHANNEL_COUNT" 21 | private const val ID_AUDIO_FORMAT: String = "ID_AUDIO_FORMAT" 22 | 23 | 24 | /** 25 | * Commands UI -> Service 26 | */ 27 | enum class Command { 28 | StartStream, 29 | StopStream, 30 | GetStatus, 31 | 32 | // called when the ui is bind 33 | BindCheck, 34 | } 35 | 36 | fun Bundle.getOrdinal(key: String): Int? { 37 | val v = this.getInt(key, Int.MIN_VALUE); 38 | 39 | return if (v == Int.MIN_VALUE) { 40 | null 41 | } else { 42 | v 43 | } 44 | } 45 | 46 | data class CommandData( 47 | val command: Command, 48 | val mode: Mode? = null, 49 | var ip: String? = null, 50 | var port: Int? = null, 51 | val sampleRate: SampleRates? = null, 52 | val channelCount: ChannelCount? = null, 53 | val audioFormat: AudioFormat? = null, 54 | ) { 55 | 56 | companion object { 57 | fun fromMessage(msg: Message): CommandData { 58 | return CommandData( 59 | command = Command.entries[msg.what], 60 | mode = msg.data.getOrdinal(ID_MODE)?.let { Mode.entries[it] }, 61 | ip = msg.data.getString(ID_IP), 62 | port = msg.data.getInt(ID_PORT), 63 | sampleRate = msg.data.getOrdinal(ID_SAMPLE_RATE)?.let { SampleRates.entries[it] }, 64 | channelCount = msg.data.getOrdinal(ID_CHANNEL_COUNT) 65 | ?.let { ChannelCount.entries[it] }, 66 | audioFormat = msg.data.getOrdinal(ID_AUDIO_FORMAT)?.let { AudioFormat.entries[it] }, 67 | ) 68 | } 69 | } 70 | 71 | fun toCommandMsg(): Message { 72 | 73 | val r = Bundle() 74 | 75 | this.mode?.let { r.putInt(ID_MODE, it.ordinal) } 76 | 77 | this.ip?.let { r.putString(ID_IP, it) } 78 | this.port?.let { r.putInt(ID_PORT, it) } 79 | 80 | this.sampleRate?.let { r.putInt(ID_SAMPLE_RATE, it.ordinal) } 81 | this.channelCount?.let { r.putInt(ID_CHANNEL_COUNT, it.ordinal) } 82 | this.audioFormat?.let { r.putInt(ID_AUDIO_FORMAT, it.ordinal) } 83 | 84 | val reply = Message.obtain() 85 | reply.data = r 86 | reply.what = this.command.ordinal 87 | 88 | return reply 89 | } 90 | 91 | 92 | } 93 | 94 | 95 | /** 96 | * Response Service -> UI 97 | */ 98 | enum class Response { 99 | Standard, 100 | } 101 | 102 | enum class ServiceState { 103 | Connected, 104 | Disconnected, 105 | } 106 | 107 | data class ResponseData( 108 | val state: ServiceState? = null, 109 | val msg: String? = null, 110 | val kind: Response = Response.Standard, 111 | ) { 112 | 113 | 114 | companion object { 115 | fun fromMessage(msg: Message): ResponseData { 116 | return ResponseData( 117 | kind = Response.entries[msg.what], 118 | state = msg.data.getOrdinal(ID_STATE)?.let { ServiceState.entries[it] }, 119 | msg = msg.data.getString(ID_MSG) 120 | ) 121 | } 122 | } 123 | 124 | 125 | fun toResponseMsg(): Message { 126 | 127 | val r = Bundle() 128 | 129 | this.msg?.let { r.putString(ID_MSG, it) } 130 | this.state?.let { r.putInt(ID_STATE, it.ordinal) } 131 | 132 | val reply = Message.obtain() 133 | reply.data = r 134 | reply.what = kind.ordinal 135 | 136 | return reply 137 | } 138 | 139 | } 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/domain/service/Packets.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.domain.service 2 | 3 | // definition of an audio packet 4 | data class AudioPacket( 5 | val buffer: ByteArray, 6 | val sampleRate: Int, 7 | val channelCount: Int, 8 | val audioFormat: Int, 9 | ) -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/domain/service/ServiceUtil.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.domain.service 2 | 3 | 4 | import android.app.Notification 5 | import android.app.PendingIntent 6 | import android.content.Intent 7 | import android.widget.Toast 8 | import androidx.core.app.NotificationCompat 9 | import com.example.androidMic.R 10 | import com.example.androidMic.ui.MainActivity 11 | import kotlinx.coroutines.CoroutineScope 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.launch 14 | 15 | 16 | const val CHANNEL_ID = "Service" 17 | 18 | class MessageUi(private val ctx: ForegroundService) { 19 | // show message on UI 20 | fun showMessage(message: String) { 21 | CoroutineScope(Dispatchers.Main).launch { 22 | Toast.makeText(ctx, message, Toast.LENGTH_SHORT).show() 23 | } 24 | } 25 | 26 | 27 | fun getNotification(): Notification { 28 | // launch activity 29 | val launchIntent = Intent(ctx, MainActivity::class.java).apply { 30 | flags = 31 | (Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) 32 | } 33 | val pLaunchIntent = 34 | PendingIntent.getActivity(ctx, 0, launchIntent, PendingIntent.FLAG_IMMUTABLE) 35 | 36 | 37 | val builder = NotificationCompat.Builder(ctx, CHANNEL_ID) 38 | .setSmallIcon(R.mipmap.ic_launcher) 39 | .setContentTitle(ctx.getString(R.string.app_name)) 40 | .setContentText(ctx.getString(R.string.notification_info)) 41 | .setPriority(NotificationCompat.PRIORITY_DEFAULT) 42 | .setContentIntent(pLaunchIntent) 43 | .setOngoing(true) 44 | 45 | return builder.build() 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/domain/streaming/AdbStreamer.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.domain.streaming 2 | 3 | import Message 4 | import android.os.Messenger 5 | import android.util.Log 6 | import com.example.androidMic.domain.service.AudioPacket 7 | import com.example.androidMic.domain.service.Command 8 | import com.example.androidMic.domain.service.CommandData 9 | import com.example.androidMic.utils.toBigEndianU32 10 | import com.google.protobuf.ByteString 11 | import kotlinx.coroutines.CoroutineScope 12 | import kotlinx.coroutines.Job 13 | import kotlinx.coroutines.delay 14 | import kotlinx.coroutines.flow.Flow 15 | import kotlinx.coroutines.launch 16 | import java.io.IOException 17 | import java.net.InetSocketAddress 18 | import java.net.Socket 19 | import java.net.SocketTimeoutException 20 | 21 | class AdbStreamer(private val scope: CoroutineScope) : Streamer { 22 | private val TAG: String = "UsbAdbStreamer" 23 | 24 | private val MAX_WAIT_TIME = 1500 // timeout 25 | 26 | private var socket: Socket? = null 27 | private val address: String = "127.0.0.1" 28 | private var streamJob: Job? = null 29 | 30 | // connect to server 31 | override fun connect(): Boolean { 32 | // create socket 33 | socket = Socket() 34 | try { 35 | socket?.connect(InetSocketAddress(address, 55555), MAX_WAIT_TIME) 36 | } catch (e: IOException) { 37 | Log.d(TAG, "connect [Socket]: ${e.message}") 38 | null 39 | } catch (e: SocketTimeoutException) { 40 | Log.d(TAG, "connect [Socket]: ${e.message}") 41 | null 42 | } catch (e: Exception) { 43 | Log.d(TAG, "connect [Socket]: ${e.message}") 44 | null 45 | } ?: return false 46 | socket?.soTimeout = MAX_WAIT_TIME 47 | return true 48 | } 49 | 50 | // stream data through socket 51 | override fun start(audioStream: Flow, tx: Messenger) { 52 | streamJob?.cancel() 53 | 54 | streamJob = scope.launch { 55 | audioStream.collect { data -> 56 | if (socket == null || socket?.isConnected != true) return@collect 57 | 58 | try { 59 | val message = Message.AudioPacketMessage.newBuilder() 60 | .setBuffer(ByteString.copyFrom(data.buffer)) 61 | .setSampleRate(data.sampleRate) 62 | .setAudioFormat(data.audioFormat) 63 | .setChannelCount(data.channelCount) 64 | .build() 65 | val pack = message.toByteArray() 66 | 67 | // Log.d(TAG, "audio buffer size = ${message.buffer.size()}") 68 | socket!!.outputStream.write(pack.size.toBigEndianU32()) 69 | socket!!.outputStream.write(message.toByteArray()) 70 | socket!!.outputStream.flush() 71 | } catch (e: IOException) { 72 | Log.d(TAG, "${e.message}") 73 | delay(5) 74 | disconnect() 75 | tx.send(CommandData(Command.StopStream).toCommandMsg()) 76 | } catch (e: Exception) { 77 | Log.d(TAG, "${e.message}") 78 | } 79 | } 80 | } 81 | } 82 | 83 | // disconnect from server 84 | override fun disconnect(): Boolean { 85 | if (socket == null) return false 86 | try { 87 | socket?.close() 88 | } catch (e: IOException) { 89 | Log.d(TAG, "disconnect [close]: ${e.message}") 90 | socket = null 91 | return false 92 | } 93 | socket = null 94 | streamJob?.cancel() 95 | streamJob = null 96 | Log.d(TAG, "disconnect: complete") 97 | return true 98 | } 99 | 100 | // shutdown streamer 101 | override fun shutdown() { 102 | disconnect() 103 | } 104 | 105 | // get connected server information 106 | override fun getInfo(): String { 107 | if (socket == null) return "" 108 | return "[Device Address]:${socket?.remoteSocketAddress}" 109 | } 110 | 111 | // return true if is connected for streaming 112 | override fun isAlive(): Boolean { 113 | return (socket != null && socket?.isConnected == true) 114 | } 115 | 116 | } -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/domain/streaming/MicStreamManager.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.domain.streaming 2 | 3 | import android.content.Context 4 | import android.os.Messenger 5 | import com.example.androidMic.Mode 6 | import com.example.androidMic.domain.service.AudioPacket 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | // manage streaming data 11 | class MicStreamManager( 12 | ctx: Context, 13 | val scope: CoroutineScope, 14 | val mode: Mode, 15 | ip: String?, 16 | port: Int? 17 | ) { 18 | 19 | private var streamer: Streamer = when (mode) { 20 | Mode.WIFI -> { 21 | WifiStreamer(ctx, scope, ip!!, port!!) 22 | } 23 | 24 | Mode.ADB -> { 25 | AdbStreamer(scope) 26 | } 27 | 28 | Mode.USB -> { 29 | UsbStreamer(ctx, scope) 30 | } 31 | 32 | Mode.UDP -> { 33 | UdpStreamer(scope, ip!!, port!!) 34 | } 35 | } 36 | 37 | 38 | companion object { 39 | const val STREAM_DELAY = 1L 40 | } 41 | 42 | fun start(audioStream: Flow, tx: Messenger): Boolean { 43 | val connected = streamer.connect() 44 | if (connected) { 45 | streamer.start(audioStream, tx) 46 | } 47 | return connected 48 | } 49 | 50 | fun stop() { 51 | streamer.disconnect() 52 | } 53 | 54 | // should not call any methods after calling 55 | fun shutdown() { 56 | streamer.shutdown() 57 | } 58 | 59 | fun getInfo(): String { 60 | return "[Streaming Mode] ${mode.name}\n${streamer.getInfo()}" 61 | } 62 | 63 | fun isConnected(): Boolean { 64 | return streamer.isAlive() 65 | } 66 | } -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/domain/streaming/Streamer.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.domain.streaming 2 | 3 | import android.os.Messenger 4 | import com.example.androidMic.domain.service.AudioPacket 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface Streamer { 8 | fun connect(): Boolean 9 | fun disconnect(): Boolean 10 | fun shutdown() 11 | fun start(audioStream: Flow, tx: Messenger) 12 | fun getInfo(): String 13 | fun isAlive(): Boolean 14 | } -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/domain/streaming/UdpStreamer.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.domain.streaming 2 | 3 | import android.os.Messenger 4 | import android.util.Log 5 | import com.example.androidMic.domain.service.AudioPacket 6 | import com.example.androidMic.utils.toBigEndianU32 7 | import com.google.protobuf.ByteString 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.Job 10 | import kotlinx.coroutines.flow.Flow 11 | import kotlinx.coroutines.launch 12 | import java.net.DatagramPacket 13 | import java.net.DatagramSocket 14 | import java.net.InetAddress 15 | 16 | class UdpStreamer(private val scope: CoroutineScope, val ip: String, val port: Int) : Streamer { 17 | 18 | private val TAG: String = "UDP streamer" 19 | 20 | private val socket: DatagramSocket = DatagramSocket() 21 | private val address = InetAddress.getByName(ip) 22 | private var streamJob: Job? = null 23 | private var sequenceIdx = 0 24 | 25 | override fun connect(): Boolean { 26 | return true 27 | } 28 | 29 | override fun disconnect(): Boolean { 30 | streamJob?.cancel() 31 | streamJob = null 32 | Log.d(TAG, "disconnect: complete") 33 | return true 34 | } 35 | 36 | override fun shutdown() { 37 | disconnect() 38 | } 39 | 40 | override fun start(audioStream: Flow, tx: Messenger) { 41 | streamJob?.cancel() 42 | 43 | streamJob = scope.launch { 44 | audioStream.collect { data -> 45 | try { 46 | val message = Message.AudioPacketMessageOrdered.newBuilder() 47 | .setSequenceNumber(sequenceIdx++) 48 | .setAudioPacket( 49 | Message.AudioPacketMessage.newBuilder() 50 | .setBuffer(ByteString.copyFrom(data.buffer)) 51 | .setSampleRate(data.sampleRate) 52 | .setAudioFormat(data.audioFormat) 53 | .setChannelCount(data.channelCount) 54 | .build() 55 | ) 56 | .build() 57 | 58 | val pack = message.toByteArray() 59 | val combined = pack.size.toBigEndianU32() + pack 60 | 61 | val packet = DatagramPacket( 62 | combined, 63 | 0, 64 | combined.size, 65 | address, 66 | port 67 | ) 68 | 69 | socket.send(packet) 70 | } catch (e: Exception) { 71 | Log.d(TAG, "stream: ${e.message}") 72 | } 73 | } 74 | } 75 | } 76 | 77 | override fun getInfo(): String { 78 | return "[Device Address]:${ip}" 79 | } 80 | 81 | override fun isAlive(): Boolean { 82 | return true 83 | } 84 | } -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/domain/streaming/UsbStreamer.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.domain.streaming 2 | 3 | import Message 4 | import android.app.PendingIntent 5 | import android.content.BroadcastReceiver 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.content.IntentFilter 9 | import android.hardware.usb.UsbAccessory 10 | import android.hardware.usb.UsbManager 11 | import android.os.Build 12 | import android.os.Messenger 13 | import android.os.ParcelFileDescriptor 14 | import android.util.Log 15 | import androidx.core.os.BundleCompat 16 | import com.example.androidMic.domain.service.AudioPacket 17 | import com.example.androidMic.utils.toBigEndianU32 18 | import com.google.protobuf.ByteString 19 | import kotlinx.coroutines.CoroutineScope 20 | import kotlinx.coroutines.Job 21 | import kotlinx.coroutines.flow.Flow 22 | import kotlinx.coroutines.launch 23 | import java.io.FileDescriptor 24 | import java.io.FileInputStream 25 | import java.io.FileOutputStream 26 | 27 | class UsbStreamer(ctx: Context, private val scope: CoroutineScope) : Streamer { 28 | 29 | private val TAG: String = "USB streamer" 30 | private val USB_PERMISSION = "com.example.androidMic.USB_PERMISSION" 31 | 32 | private var streamJob: Job? = null 33 | 34 | private var accessory: UsbAccessory? = null 35 | private var accessoryPfd: ParcelFileDescriptor? = null 36 | private var accessoryFd: FileDescriptor? = null 37 | private var outputStream: FileOutputStream? = null 38 | private var inputStream: FileInputStream? = null 39 | 40 | private val receiver = object : BroadcastReceiver() { 41 | override fun onReceive(context: Context?, intent: Intent?) { 42 | val action = intent?.action ?: return 43 | 44 | if (action == UsbManager.ACTION_USB_ACCESSORY_DETACHED) { 45 | Log.d(TAG, "onReceive: USB accessory detached") 46 | 47 | val acc = BundleCompat.getParcelable( 48 | intent.extras!!, 49 | UsbManager.EXTRA_ACCESSORY, 50 | UsbAccessory::class.java, 51 | ) 52 | 53 | if (acc == accessory) { 54 | shutdown() 55 | } 56 | } else if (action == USB_PERMISSION) { 57 | val granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false) 58 | if (granted) { 59 | Log.d(TAG, "permission granted") 60 | 61 | val usbManager = ctx.getSystemService(Context.USB_SERVICE) as UsbManager 62 | 63 | val pfd = usbManager.openAccessory(accessory) 64 | val fd = pfd?.fileDescriptor 65 | 66 | if (fd == null) { 67 | Log.d(TAG, "Failed to open USB accessory file descriptor") 68 | return 69 | } 70 | accessoryPfd = pfd 71 | accessoryFd = fd 72 | outputStream = FileOutputStream(fd) 73 | inputStream = FileInputStream(fd) 74 | } else { 75 | Log.d(TAG, "permission denied") 76 | } 77 | } 78 | } 79 | } 80 | 81 | // init everything 82 | init { 83 | // set up filter 84 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 85 | val filter = IntentFilter( 86 | UsbManager.ACTION_USB_ACCESSORY_DETACHED 87 | ) 88 | filter.addAction(USB_PERMISSION) 89 | ctx.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED) 90 | } else { 91 | val filter = IntentFilter( 92 | UsbManager.ACTION_USB_ACCESSORY_DETACHED 93 | ) 94 | ctx.registerReceiver(receiver, filter) 95 | } 96 | 97 | // select usb device 98 | val usbManager = ctx.getSystemService(Context.USB_SERVICE) as UsbManager 99 | val accessoryList = usbManager.accessoryList 100 | 101 | require(!accessoryList.isNullOrEmpty()) { 102 | "No USB device detected" 103 | } 104 | 105 | accessory = accessoryList[0]; 106 | 107 | Log.d( 108 | TAG, 109 | "choose USB accessory: ${accessory?.manufacturer} ${accessory?.model} ${accessory?.version}" 110 | ) 111 | 112 | // check permission 113 | if (!usbManager.hasPermission(accessory)) { 114 | Log.d(TAG, "requesting permission") 115 | usbManager.requestPermission( 116 | accessory, PendingIntent.getBroadcast( 117 | ctx, 0, Intent(USB_PERMISSION), 0 118 | ) 119 | ) 120 | } 121 | 122 | require(usbManager.hasPermission(accessory)) { 123 | "Failed to get permission for USB accessory" 124 | } 125 | 126 | // open stream 127 | val pfd = usbManager.openAccessory(accessory) 128 | val fd = pfd?.fileDescriptor 129 | require(fd != null) { 130 | "Failed to open USB accessory file descriptor" 131 | } 132 | accessoryPfd = pfd 133 | accessoryFd = fd 134 | outputStream = FileOutputStream(fd) 135 | inputStream = FileInputStream(fd) 136 | } 137 | 138 | override fun connect(): Boolean { 139 | return true 140 | } 141 | 142 | override fun disconnect(): Boolean { 143 | streamJob?.cancel() 144 | streamJob = null 145 | Log.d(TAG, "disconnect: complete") 146 | return true 147 | } 148 | 149 | override fun shutdown() { 150 | outputStream?.close() 151 | disconnect() 152 | } 153 | 154 | override fun start(audioStream: Flow, tx: Messenger) { 155 | streamJob?.cancel() 156 | 157 | streamJob = scope.launch { 158 | audioStream.collect { data -> 159 | if (accessory == null || outputStream == null) return@collect 160 | 161 | try { 162 | val message = Message.AudioPacketMessage.newBuilder() 163 | .setBuffer(ByteString.copyFrom(data.buffer)) 164 | .setSampleRate(data.sampleRate) 165 | .setAudioFormat(data.audioFormat) 166 | .setChannelCount(data.channelCount) 167 | .build() 168 | 169 | val pack = message.toByteArray() 170 | 171 | // Log.d(TAG, "usb stream: sending ${pack.size} bytes") 172 | 173 | outputStream!!.write(pack.size.toBigEndianU32()) 174 | outputStream!!.write(pack) 175 | outputStream!!.flush() 176 | } catch (e: Exception) { 177 | Log.d(TAG, "stream: ${e.message}") 178 | } 179 | } 180 | } 181 | } 182 | 183 | override fun getInfo(): String { 184 | if (accessory == null) return "No USB device detected" 185 | return "[USB Accessory Model]:${accessory?.model}\n[Manufacturer]:${accessory?.manufacturer}\n[Version]:${accessory?.version}" 186 | } 187 | 188 | override fun isAlive(): Boolean { 189 | return true 190 | } 191 | } -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/domain/streaming/WifiStreamer.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.domain.streaming 2 | 3 | import android.content.Context 4 | import android.net.ConnectivityManager 5 | import android.os.Messenger 6 | import android.util.Log 7 | import com.example.androidMic.domain.service.AudioPacket 8 | import com.example.androidMic.utils.toBigEndianU32 9 | import com.google.protobuf.ByteString 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.Job 12 | import kotlinx.coroutines.delay 13 | import kotlinx.coroutines.flow.Flow 14 | import kotlinx.coroutines.launch 15 | import java.io.IOException 16 | import java.net.InetSocketAddress 17 | import java.net.Socket 18 | import java.net.SocketTimeoutException 19 | 20 | class WifiStreamer( 21 | ctx: Context, 22 | private val scope: CoroutineScope, 23 | ip: String, 24 | port: Int 25 | ) : Streamer { 26 | private val TAG: String = "MicStreamWIFI" 27 | 28 | private val MAX_WAIT_TIME = 1500 // timeout 29 | 30 | private var socket: Socket? = null 31 | private var address: String 32 | private val port: Int 33 | private var streamJob: Job? = null 34 | 35 | init { 36 | // check WIFI 37 | // reference: https://stackoverflow.com/questions/70107145/connectivity-manager-allnetworks-deprecated 38 | val cm = ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 39 | val net = cm.activeNetwork 40 | require(net != null) { 41 | "Wifi not available" 42 | } 43 | require(cm.getNetworkCapabilities(net) != null) { 44 | "Wifi not available" 45 | } 46 | 47 | val inetSocketAddress = InetSocketAddress(ip, port) 48 | this.address = inetSocketAddress.hostName 49 | this.port = inetSocketAddress.port 50 | } 51 | 52 | // connect to server 53 | override fun connect(): Boolean { 54 | // create socket 55 | socket = Socket() 56 | try { 57 | socket?.connect(InetSocketAddress(address, port), MAX_WAIT_TIME) 58 | } catch (e: IOException) { 59 | Log.d(TAG, "connect [Socket]: ${e.message}") 60 | null 61 | } catch (e: SocketTimeoutException) { 62 | Log.d(TAG, "connect [Socket]: ${e.message}") 63 | null 64 | } ?: return false 65 | socket?.soTimeout = MAX_WAIT_TIME 66 | 67 | return true 68 | } 69 | 70 | // stream data through socket 71 | override fun start(audioStream: Flow, tx: Messenger) { 72 | streamJob?.cancel() 73 | 74 | streamJob = scope.launch { 75 | audioStream.collect { data -> 76 | if (socket == null || socket?.isConnected != true) return@collect 77 | 78 | try { 79 | val message = Message.AudioPacketMessage.newBuilder() 80 | .setBuffer(ByteString.copyFrom(data.buffer)) 81 | .setSampleRate(data.sampleRate) 82 | .setAudioFormat(data.audioFormat) 83 | .setChannelCount(data.channelCount) 84 | .build() 85 | val pack = message.toByteArray() 86 | 87 | socket!!.outputStream.write(pack.size.toBigEndianU32()) 88 | socket!!.outputStream.write(message.toByteArray()) 89 | socket!!.outputStream.flush() 90 | } catch (e: IOException) { 91 | Log.d(TAG, "${e.message}") 92 | delay(5) 93 | disconnect() 94 | } catch (e: Exception) { 95 | Log.d(TAG, "${e.message}") 96 | } 97 | } 98 | } 99 | } 100 | 101 | // disconnect from server 102 | override fun disconnect(): Boolean { 103 | if (socket == null) return false 104 | try { 105 | socket?.close() 106 | } catch (e: IOException) { 107 | Log.d(TAG, "disconnect [close]: ${e.message}") 108 | socket = null 109 | return false 110 | } 111 | socket = null 112 | streamJob?.cancel() 113 | streamJob = null 114 | Log.d(TAG, "disconnect: complete") 115 | return true 116 | } 117 | 118 | // shutdown streamer 119 | override fun shutdown() { 120 | disconnect() 121 | address = "" 122 | } 123 | 124 | // get connected server information 125 | override fun getInfo(): String { 126 | if (socket == null) return "" 127 | return "[Device Address]:${socket?.remoteSocketAddress}" 128 | } 129 | 130 | // return true if is connected for streaming 131 | override fun isAlive(): Boolean { 132 | return (socket != null && socket?.isConnected == true) 133 | } 134 | } -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.ui 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.util.Log 6 | import androidx.activity.ComponentActivity 7 | import androidx.activity.compose.setContent 8 | import androidx.activity.viewModels 9 | import com.example.androidMic.AndroidMicApp 10 | import com.example.androidMic.ui.home.HomeScreen 11 | import com.example.androidMic.ui.theme.AndroidMicTheme 12 | import com.example.androidMic.ui.utils.rememberWindowInfo 13 | import com.example.androidMic.utils.ignore 14 | 15 | 16 | class MainActivity : ComponentActivity() { 17 | private val TAG = "MainActivity" 18 | 19 | private val WAIT_PERIOD = 500L 20 | 21 | val vm: MainViewModel by viewModels() 22 | 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | super.onCreate(savedInstanceState) 25 | Log.d(TAG, "onCreate") 26 | 27 | setContent { 28 | 29 | AndroidMicTheme( 30 | theme = vm.prefs.theme.getAsState().value, 31 | dynamicColor = vm.prefs.dynamicColor.getAsState().value 32 | ) { 33 | // get windowInfo for rotation change 34 | val windowInfo = rememberWindowInfo() 35 | 36 | HomeScreen(vm, windowInfo) 37 | } 38 | } 39 | } 40 | 41 | override fun onNewIntent(intent: Intent) { 42 | super.onNewIntent(intent) 43 | if (intent.extras?.getBoolean("ForegroundServiceBound") == true) { 44 | Log.d(TAG, "onNewIntent -> ForegroundServiceBound") 45 | // get variable from application 46 | vm.refreshAppVariables() 47 | // get status 48 | vm.askForStatus() 49 | } 50 | } 51 | 52 | override fun onStart() { 53 | super.onStart() 54 | Log.d(TAG, "onStart") 55 | 56 | 57 | vm.handlerServiceResponse() 58 | // get variable from application 59 | vm.refreshAppVariables() 60 | 61 | (application as AndroidMicApp).bindService() 62 | } 63 | 64 | 65 | override fun onStop() { 66 | super.onStop() 67 | Log.d(TAG, "onStop") 68 | vm.mMessengerLooper.quitSafely() 69 | ignore { vm.handlerThread.join(WAIT_PERIOD) } 70 | 71 | (application as AndroidMicApp).unBindService() 72 | } 73 | 74 | } -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/ui/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.ui 2 | 3 | import android.os.Handler 4 | import android.os.HandlerThread 5 | import android.os.Looper 6 | import android.os.Message 7 | import android.os.Messenger 8 | import android.os.Process 9 | import android.util.Log 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.lifecycle.ViewModel 12 | import androidx.lifecycle.viewModelScope 13 | import com.example.androidMic.AndroidMicApp 14 | import com.example.androidMic.AppPreferences 15 | import com.example.androidMic.AudioFormat 16 | import com.example.androidMic.ChannelCount 17 | import com.example.androidMic.Dialogs 18 | import com.example.androidMic.Mode 19 | import com.example.androidMic.R 20 | import com.example.androidMic.SampleRates 21 | import com.example.androidMic.Themes 22 | import com.example.androidMic.domain.service.Command 23 | import com.example.androidMic.domain.service.CommandData 24 | import com.example.androidMic.domain.service.Response 25 | import com.example.androidMic.domain.service.ResponseData 26 | import com.example.androidMic.domain.service.ServiceState 27 | import com.example.androidMic.ui.utils.UiHelper 28 | import com.example.androidMic.utils.checkIp 29 | import com.example.androidMic.utils.checkPort 30 | import kotlinx.coroutines.launch 31 | 32 | 33 | class MainViewModel : ViewModel() { 34 | 35 | private val TAG = "MainViewModel" 36 | 37 | 38 | val prefs: AppPreferences = AndroidMicApp.appModule.appPreferences 39 | val uiHelper: UiHelper = AndroidMicApp.appModule.uiHelper 40 | 41 | private var mService: Messenger? = null 42 | private var mBound = false 43 | 44 | lateinit var handlerThread: HandlerThread 45 | private lateinit var mMessenger: Messenger 46 | lateinit var mMessengerLooper: Looper 47 | private lateinit var mMessengerHandler: ReplyHandler 48 | 49 | 50 | val textLog = mutableStateOf("") 51 | 52 | val isStreamStarted = mutableStateOf(false) 53 | val isButtonConnectClickable = mutableStateOf(false) 54 | 55 | init { 56 | Log.d(TAG, "init") 57 | } 58 | 59 | private inner class ReplyHandler(looper: Looper) : Handler(looper) { 60 | override fun handleMessage(msg: Message) { 61 | 62 | val data = ResponseData.fromMessage(msg); 63 | 64 | when (Response.entries[msg.what]) { 65 | Response.Standard -> { 66 | data.state?.let { 67 | isButtonConnectClickable.value = true 68 | isStreamStarted.value = it == ServiceState.Connected 69 | } 70 | 71 | data.msg?.let { 72 | addLogMessage(it) 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | fun handlerServiceResponse() { 80 | handlerThread = HandlerThread("activity", Process.THREAD_PRIORITY_BACKGROUND) 81 | handlerThread.start() 82 | mMessengerLooper = handlerThread.looper 83 | mMessengerHandler = ReplyHandler(mMessengerLooper) 84 | mMessenger = Messenger(mMessengerHandler) 85 | } 86 | 87 | fun refreshAppVariables() { 88 | mBound = AndroidMicApp.mBound 89 | mService = AndroidMicApp.mService 90 | 91 | isStreamStarted.value = false 92 | isButtonConnectClickable.value = false 93 | 94 | val msg = CommandData(Command.BindCheck).toCommandMsg() 95 | msg.replyTo = mMessenger 96 | mService?.send(msg) 97 | } 98 | 99 | fun onConnectButton(): Dialogs? { 100 | if (!mBound) return null 101 | val reply = if (isStreamStarted.value) { 102 | Log.d(TAG, "onConnectButton: stop stream") 103 | CommandData(Command.StopStream).toCommandMsg() 104 | } else { 105 | val ip = prefs.ip.getBlocking() 106 | val port = prefs.port.getBlocking() 107 | val mode = prefs.mode.getBlocking() 108 | 109 | val data = CommandData( 110 | command = Command.StartStream, 111 | sampleRate = prefs.sampleRate.getBlocking(), 112 | channelCount = prefs.channelCount.getBlocking(), 113 | audioFormat = prefs.audioFormat.getBlocking(), 114 | mode = mode 115 | ) 116 | 117 | when (mode) { 118 | Mode.WIFI, Mode.UDP -> { 119 | if (!checkIp(ip) || !checkPort(port)) { 120 | uiHelper.makeToast( 121 | uiHelper.getString(R.string.invalid_ip_port) 122 | ) 123 | return Dialogs.IpPort 124 | } 125 | data.ip = ip 126 | data.port = port.toInt() 127 | } 128 | 129 | else -> {} 130 | } 131 | 132 | Log.d(TAG, "onConnectButton: start stream") 133 | // lock button to avoid duplicate events 134 | isButtonConnectClickable.value = false 135 | 136 | data.toCommandMsg() 137 | } 138 | 139 | reply.replyTo = mMessenger 140 | mService?.send(reply) 141 | 142 | return null 143 | } 144 | 145 | fun setIpPort(ip: String, port: String) { 146 | viewModelScope.launch { 147 | prefs.ip.update(ip) 148 | prefs.port.update(port) 149 | } 150 | } 151 | 152 | fun setMode(mode: Mode) { 153 | viewModelScope.launch { 154 | prefs.mode.update(mode) 155 | } 156 | } 157 | 158 | fun setSampleRate(sampleRate: SampleRates) { 159 | viewModelScope.launch { 160 | prefs.sampleRate.update(sampleRate) 161 | } 162 | } 163 | 164 | fun setChannelCount(channelCount: ChannelCount) { 165 | viewModelScope.launch { 166 | prefs.channelCount.update(channelCount) 167 | } 168 | } 169 | 170 | fun setAudioFormat(audioFormat: AudioFormat) { 171 | viewModelScope.launch { 172 | prefs.audioFormat.update(audioFormat) 173 | } 174 | } 175 | 176 | 177 | fun setTheme(theme: Themes) { 178 | viewModelScope.launch { 179 | prefs.theme.update(theme) 180 | } 181 | } 182 | 183 | fun setDynamicColor(dynamicColor: Boolean) { 184 | viewModelScope.launch { 185 | prefs.dynamicColor.update(dynamicColor) 186 | } 187 | } 188 | 189 | fun cleanLog() { 190 | textLog.value = "" 191 | } 192 | 193 | 194 | // ask foreground service for current status 195 | fun askForStatus() { 196 | if (!mBound) return 197 | val reply = Message.obtain() 198 | reply.what = Command.GetStatus.ordinal 199 | reply.replyTo = mMessenger 200 | mService?.send(reply) 201 | } 202 | 203 | 204 | // helper function to append log message to textview 205 | private fun addLogMessage(message: String) { 206 | textLog.value = textLog.value + message + "\n" 207 | } 208 | } -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/ui/components/Components.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.ui.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.foundation.text.KeyboardOptions 11 | import androidx.compose.material3.Button 12 | import androidx.compose.material3.ButtonDefaults 13 | import androidx.compose.material3.Checkbox 14 | import androidx.compose.material3.CheckboxDefaults 15 | import androidx.compose.material3.ExperimentalMaterial3Api 16 | import androidx.compose.material3.MaterialTheme 17 | import androidx.compose.material3.OutlinedTextField 18 | import androidx.compose.material3.OutlinedTextFieldDefaults 19 | import androidx.compose.material3.Surface 20 | import androidx.compose.material3.Text 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.runtime.MutableState 23 | import androidx.compose.ui.Alignment 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.res.stringResource 26 | import androidx.compose.ui.text.input.KeyboardType 27 | import androidx.compose.ui.unit.dp 28 | 29 | @Composable 30 | fun ManagerButton( 31 | onClick: () -> Unit, 32 | text: String, 33 | modifier: Modifier = Modifier, 34 | enabled: Boolean = true 35 | ) { 36 | Button( 37 | shape = RoundedCornerShape(20), 38 | colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), 39 | onClick = onClick, 40 | modifier = modifier, 41 | enabled = enabled 42 | ) { 43 | Text( 44 | text = text, 45 | color = MaterialTheme.colorScheme.onPrimary, 46 | style = MaterialTheme.typography.labelLarge 47 | ) 48 | } 49 | } 50 | 51 | 52 | @Composable 53 | fun ManagerCheckBox( 54 | checked: Boolean, 55 | onClick: (Boolean) -> Unit, 56 | text: String, 57 | modifier: Modifier = Modifier 58 | ) { 59 | Surface( 60 | shape = RoundedCornerShape(20), 61 | color = MaterialTheme.colorScheme.primary, 62 | modifier = modifier 63 | .fillMaxWidth(0.8f) 64 | .clickable { 65 | onClick(!checked) 66 | } 67 | ) { 68 | Row( 69 | verticalAlignment = Alignment.CenterVertically 70 | ) { 71 | Checkbox( 72 | checked = checked, 73 | onCheckedChange = { onClick(it) }, 74 | colors = CheckboxDefaults.colors( 75 | checkedColor = MaterialTheme.colorScheme.onPrimary, 76 | uncheckedColor = MaterialTheme.colorScheme.onPrimary, 77 | checkmarkColor = MaterialTheme.colorScheme.primary 78 | ) 79 | ) 80 | 81 | Spacer(modifier = Modifier.width(10.dp)) 82 | 83 | Text( 84 | text = text, 85 | color = MaterialTheme.colorScheme.onPrimary, 86 | style = MaterialTheme.typography.labelLarge 87 | ) 88 | } 89 | } 90 | } 91 | 92 | 93 | @OptIn(ExperimentalMaterial3Api::class) 94 | @Composable 95 | fun ManagerOutlinedTextField(temp: MutableState, id: Int) { 96 | OutlinedTextField( 97 | modifier = Modifier.padding(horizontal = 10.dp), 98 | value = temp.value, 99 | onValueChange = { temp.value = it }, 100 | enabled = true, 101 | singleLine = true, 102 | label = { Text(stringResource(id = id)) }, 103 | textStyle = MaterialTheme.typography.bodyMedium, 104 | colors = OutlinedTextFieldDefaults.colors( 105 | focusedTextColor = MaterialTheme.colorScheme.onSurface, 106 | focusedContainerColor = MaterialTheme.colorScheme.surface 107 | ), 108 | keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number) 109 | ) 110 | } 111 | 112 | 113 | -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/ui/home/AppBar.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.ui.home 2 | 3 | 4 | import androidx.compose.foundation.layout.size 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.rounded.Menu 7 | import androidx.compose.material3.CenterAlignedTopAppBar 8 | import androidx.compose.material3.ExperimentalMaterial3Api 9 | import androidx.compose.material3.Icon 10 | import androidx.compose.material3.IconButton 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Text 13 | import androidx.compose.material3.TopAppBarDefaults 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.res.stringResource 17 | import androidx.compose.ui.unit.dp 18 | import com.example.androidMic.R 19 | 20 | 21 | @OptIn(ExperimentalMaterial3Api::class) 22 | @Composable 23 | fun AppBar( 24 | onNavigationIconClick: () -> Unit, 25 | modifier: Modifier 26 | ) { 27 | CenterAlignedTopAppBar( 28 | modifier = modifier, 29 | title = { 30 | Text( 31 | text = stringResource(id = R.string.app_name), 32 | style = MaterialTheme.typography.titleMedium 33 | ) 34 | }, 35 | navigationIcon = { 36 | IconButton(onClick = onNavigationIconClick) { 37 | Icon( 38 | imageVector = Icons.Rounded.Menu, 39 | contentDescription = "Toggle drawer", 40 | modifier = Modifier.size(25.dp) 41 | ) 42 | } 43 | }, 44 | colors = TopAppBarDefaults.mediumTopAppBarColors( 45 | containerColor = MaterialTheme.colorScheme.primary, 46 | titleContentColor = MaterialTheme.colorScheme.onPrimary, 47 | navigationIconContentColor = MaterialTheme.colorScheme.onPrimary 48 | ) 49 | ) 50 | } -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/ui/home/dialog/BaseDialog.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.ui.home.dialog 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.rememberScrollState 9 | import androidx.compose.foundation.verticalScroll 10 | import androidx.compose.material3.HorizontalDivider 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Surface 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.MutableState 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.unit.dp 18 | import androidx.compose.ui.window.Dialog 19 | import com.example.androidMic.ui.components.ManagerButton 20 | 21 | @Composable 22 | fun BaseDialog( 23 | expanded: MutableState, 24 | onDismissRequest: (() -> Unit)? = null, 25 | content: @Composable () -> Unit 26 | ) { 27 | if (expanded.value) { 28 | Dialog( 29 | onDismissRequest = { 30 | onDismissRequest?.invoke() 31 | expanded.value = false 32 | } 33 | ) { 34 | Surface( 35 | modifier = Modifier 36 | .fillMaxWidth(0.9f) 37 | .verticalScroll(rememberScrollState()), 38 | shape = MaterialTheme.shapes.medium, 39 | color = MaterialTheme.colorScheme.surface, 40 | contentColor = MaterialTheme.colorScheme.onSurface 41 | ) { 42 | Column( 43 | horizontalAlignment = Alignment.CenterHorizontally 44 | ) { 45 | 46 | Spacer(modifier = Modifier.height(25.dp)) 47 | content() 48 | Spacer(modifier = Modifier.height(25.dp)) 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | @Composable 56 | fun DialogSpacer() { 57 | Spacer(modifier = Modifier.height(20.dp)) 58 | } 59 | 60 | @Composable 61 | fun DialogDivider() { 62 | HorizontalDivider( 63 | modifier = Modifier.padding(20.dp), 64 | color = MaterialTheme.colorScheme.onSurface 65 | ) 66 | } 67 | 68 | 69 | @Composable 70 | fun DialogList( 71 | expanded: MutableState, 72 | enum: List, 73 | onClick: (E) -> Unit, 74 | text: (E) -> String, 75 | bottomContent: (@Composable () -> Unit)? = null 76 | ) { 77 | 78 | BaseDialog( 79 | expanded 80 | ) { 81 | enum.forEachIndexed { index, item -> 82 | ManagerButton( 83 | onClick = { 84 | onClick(item) 85 | expanded.value = false 86 | }, 87 | text = text(item), 88 | modifier = Modifier.fillMaxWidth(0.8f) 89 | ) 90 | 91 | if (index != enum.indices.last) { 92 | DialogSpacer() 93 | } 94 | } 95 | bottomContent?.invoke() 96 | } 97 | 98 | } -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/ui/home/dialog/audio.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.ui.home.dialog 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.MutableState 5 | import com.example.androidMic.AudioFormat 6 | import com.example.androidMic.ChannelCount 7 | import com.example.androidMic.SampleRates 8 | import com.example.androidMic.ui.MainViewModel 9 | 10 | @Composable 11 | fun DialogSampleRate( 12 | vm: MainViewModel, 13 | expanded: MutableState, 14 | ) { 15 | DialogList( 16 | expanded, 17 | enum = SampleRates.entries, 18 | onClick = { vm.setSampleRate(it) }, 19 | text = { it.value.toString() } 20 | ) 21 | } 22 | 23 | @Composable 24 | fun DialogChannelCount( 25 | vm: MainViewModel, 26 | expanded: MutableState, 27 | ) { 28 | DialogList( 29 | expanded, 30 | enum = ChannelCount.entries, 31 | onClick = { vm.setChannelCount(it) }, 32 | text = { it.toString() } 33 | ) 34 | } 35 | 36 | @Composable 37 | fun DialogAudioFormat( 38 | vm: MainViewModel, 39 | expanded: MutableState, 40 | ) { 41 | DialogList( 42 | expanded, 43 | enum = AudioFormat.entries, 44 | onClick = { vm.setAudioFormat(it) }, 45 | text = { it.toString() } 46 | ) 47 | } -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/ui/home/dialog/ipPort.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.ui.home.dialog 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.MutableState 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.saveable.rememberSaveable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.res.stringResource 15 | import androidx.compose.ui.unit.dp 16 | import com.example.androidMic.DefaultStates 17 | import com.example.androidMic.R 18 | import com.example.androidMic.ui.MainViewModel 19 | import com.example.androidMic.ui.components.ManagerButton 20 | import com.example.androidMic.ui.components.ManagerOutlinedTextField 21 | 22 | @Composable 23 | fun DialogIpPort(vm: MainViewModel, expanded: MutableState) { 24 | 25 | val tempIp = rememberSaveable { 26 | mutableStateOf(vm.prefs.ip.getBlocking()) 27 | } 28 | val tempPort = rememberSaveable { 29 | mutableStateOf(vm.prefs.port.getBlocking()) 30 | } 31 | 32 | BaseDialog( 33 | expanded, 34 | ) { 35 | Column( 36 | horizontalAlignment = Alignment.End 37 | ) { 38 | // reset button 39 | ManagerButton( 40 | onClick = { 41 | tempIp.value = DefaultStates.IP; tempPort.value = DefaultStates.PORT 42 | }, 43 | text = stringResource(id = R.string.reset), 44 | modifier = Modifier.padding(end = 10.dp), 45 | ) 46 | 47 | DialogSpacer() 48 | Column( 49 | horizontalAlignment = Alignment.CenterHorizontally 50 | ) { 51 | // ip field 52 | ManagerOutlinedTextField(tempIp, R.string.dialog_ip) 53 | 54 | Spacer(modifier = Modifier.height(10.dp)) 55 | 56 | // port field 57 | ManagerOutlinedTextField(tempPort, R.string.dialog_port) 58 | 59 | Spacer(modifier = Modifier.height(20.dp)) 60 | 61 | // save Button 62 | ManagerButton( 63 | onClick = { 64 | vm.setIpPort(tempIp.value, tempPort.value) 65 | expanded.value = false 66 | }, 67 | text = stringResource(id = R.string.save), 68 | modifier = Modifier.fillMaxWidth(0.6f) 69 | ) 70 | 71 | Spacer(modifier = Modifier.height(10.dp)) 72 | 73 | // cancel Button 74 | ManagerButton( 75 | onClick = { 76 | expanded.value = false 77 | }, 78 | text = stringResource(id = R.string.cancel), 79 | modifier = Modifier.fillMaxWidth(0.6f) 80 | ) 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/ui/home/dialog/mode.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.ui.home.dialog 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.MutableState 5 | import com.example.androidMic.Mode 6 | import com.example.androidMic.ui.MainViewModel 7 | 8 | @Composable 9 | fun DialogMode( 10 | vm: MainViewModel, 11 | expanded: MutableState, 12 | ) { 13 | DialogList( 14 | expanded, 15 | enum = Mode.entries, 16 | onClick = { vm.setMode(it) }, 17 | text = { it.toString() } 18 | ) 19 | } -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/ui/home/dialog/theme.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.ui.home.dialog 2 | 3 | import android.os.Build 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.MutableState 6 | import androidx.compose.ui.res.stringResource 7 | import com.example.androidMic.R 8 | import com.example.androidMic.Themes 9 | import com.example.androidMic.ui.MainViewModel 10 | import com.example.androidMic.ui.components.ManagerCheckBox 11 | 12 | @Composable 13 | fun DialogTheme( 14 | vm: MainViewModel, 15 | expanded: MutableState, 16 | ) { 17 | DialogList( 18 | expanded, 19 | enum = Themes.entries, 20 | onClick = { vm.setTheme(it) }, 21 | text = { it.toString() } 22 | ) { 23 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S) { 24 | DialogDivider() 25 | 26 | ManagerCheckBox( 27 | checked = vm.prefs.dynamicColor.getAsState().value, 28 | onClick = { 29 | vm.setDynamicColor(it) 30 | }, 31 | text = stringResource(id = R.string.dynamic_color) 32 | ) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | 6 | val Purple500 = Color(0xFF6200EE) 7 | val Teal200 = Color(0xFF9ACFF8) 8 | val DarkGrey = Color(0xFF000023) 9 | 10 | -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.ui.theme 2 | 3 | 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.platform.LocalContext 14 | import com.example.androidMic.Themes 15 | 16 | 17 | private val DarkColorScheme = darkColorScheme( 18 | primary = Purple500, 19 | onPrimary = Color.White, 20 | 21 | secondary = Teal200, 22 | onSecondary = Color.Black, 23 | 24 | tertiary = Color.Black, 25 | onTertiary = Color.White, 26 | 27 | surface = DarkGrey, 28 | onSurface = Color.White, 29 | 30 | background = Color.Black, 31 | onBackground = Color.White, 32 | ) 33 | 34 | private val LightColorScheme = lightColorScheme( 35 | primary = Purple500, 36 | onPrimary = Color.White, 37 | 38 | secondary = Teal200, 39 | onSecondary = Color.Black, 40 | 41 | tertiary = Color.Black, 42 | onTertiary = Color.White, 43 | 44 | surface = Color.White, 45 | onSurface = Color.Black, 46 | 47 | background = Color.White, 48 | onBackground = Color.Black, 49 | ) 50 | 51 | @Composable 52 | fun AndroidMicTheme( 53 | theme: Themes, 54 | // Dynamic color is available on Android 12+ 55 | dynamicColor: Boolean, 56 | content: @Composable () -> Unit 57 | ) { 58 | val darkTheme = when (theme) { 59 | Themes.System -> isSystemInDarkTheme() 60 | Themes.Light -> false 61 | Themes.Dark -> true 62 | } 63 | val colorScheme = when { 64 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 65 | val context = LocalContext.current 66 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 67 | } 68 | 69 | darkTheme -> DarkColorScheme 70 | else -> LightColorScheme 71 | } 72 | 73 | MaterialTheme( 74 | colorScheme = colorScheme, 75 | typography = Typography, 76 | content = content 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | // setting title 12 | titleLarge = TextStyle( 13 | fontFamily = FontFamily.Default, 14 | fontWeight = FontWeight.Bold, 15 | fontSize = 60.sp, 16 | lineHeight = 24.sp, 17 | letterSpacing = 0.5.sp 18 | ), 19 | // app title 20 | titleMedium = TextStyle( 21 | fontFamily = FontFamily.Default, 22 | fontWeight = FontWeight.SemiBold, 23 | fontSize = 25.sp, 24 | lineHeight = 24.sp, 25 | letterSpacing = 0.5.sp 26 | ), 27 | 28 | // setting subtitle 29 | bodyLarge = TextStyle( 30 | fontFamily = FontFamily.Default, 31 | fontWeight = FontWeight.SemiBold, 32 | fontSize = 22.sp, 33 | lineHeight = 24.sp, 34 | letterSpacing = 0.5.sp 35 | ), 36 | // setting value 37 | bodyMedium = TextStyle( 38 | fontFamily = FontFamily.Default, 39 | fontWeight = FontWeight.Normal, 40 | fontSize = 18.sp, 41 | lineHeight = 24.sp, 42 | letterSpacing = 0.5.sp 43 | ), 44 | 45 | // switch and button 46 | labelLarge = TextStyle( 47 | fontFamily = FontFamily.Default, 48 | fontWeight = FontWeight.Normal, 49 | fontSize = 20.sp, 50 | lineHeight = 24.sp, 51 | letterSpacing = 0.5.sp 52 | ), 53 | 54 | labelMedium = TextStyle( 55 | fontFamily = FontFamily.Default, 56 | fontWeight = FontWeight.Normal, 57 | fontSize = 18.sp, 58 | lineHeight = 24.sp, 59 | letterSpacing = 0.5.sp 60 | ) 61 | ) -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/ui/utils/PermissionHelper.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.ui.utils 2 | 3 | import android.Manifest 4 | import android.os.Build 5 | 6 | fun getWifiPermission(): MutableList { 7 | val list = mutableListOf() 8 | 9 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) 10 | list.add(Manifest.permission.POST_NOTIFICATIONS) 11 | 12 | return list 13 | } 14 | 15 | fun getBluetoothPermission(): MutableList { 16 | val list = mutableListOf() 17 | 18 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) 19 | list.add(Manifest.permission.POST_NOTIFICATIONS) 20 | 21 | list.add(Manifest.permission.BLUETOOTH) 22 | 23 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) 24 | list.add(Manifest.permission.BLUETOOTH_CONNECT) 25 | 26 | return list 27 | } 28 | 29 | fun getUsbPermission(): MutableList { 30 | val list = mutableListOf() 31 | 32 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) 33 | list.add(Manifest.permission.POST_NOTIFICATIONS) 34 | 35 | return list 36 | } 37 | 38 | fun getRecordAudioPermission(): MutableList { 39 | val list = mutableListOf() 40 | 41 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) 42 | list.add(Manifest.permission.POST_NOTIFICATIONS) 43 | 44 | list.add(Manifest.permission.RECORD_AUDIO) 45 | 46 | return list 47 | } 48 | -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/ui/utils/UiHelper.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.ui.utils 2 | 3 | import android.content.Context 4 | import android.widget.Toast 5 | import androidx.annotation.PluralsRes 6 | import androidx.annotation.StringRes 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.launch 10 | 11 | class UiHelper( 12 | private val context: Context 13 | ) { 14 | fun makeToast(text: String?, duration: Int = Toast.LENGTH_SHORT) { 15 | if (text == null) return 16 | CoroutineScope(Dispatchers.Main).launch { 17 | Toast.makeText(context, text, duration).show() 18 | } 19 | } 20 | 21 | fun getString(@StringRes resId: Int): String = 22 | context.getString(resId) 23 | 24 | fun getString(@StringRes resId: Int, vararg formatArgs: Any?): String { 25 | return context.getString(resId, *formatArgs) 26 | } 27 | 28 | fun getQuantityString(@PluralsRes resId: Int, quantity: Int, vararg formatArgs: Any?): String { 29 | return context.resources.getQuantityString(resId, quantity, formatArgs) 30 | } 31 | } -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/ui/utils/ViewModelFactoryHelper.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.ui.utils 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | 6 | fun viewModelFactory(initializer: () -> VM): ViewModelProvider.Factory { 7 | return object : ViewModelProvider.Factory { 8 | override fun create(modelClass: Class): T { 9 | @Suppress("UNCHECKED_CAST") 10 | return initializer() as T 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/ui/utils/rememberWindowInfo.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.ui.utils 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.platform.LocalConfiguration 5 | import androidx.compose.ui.unit.Dp 6 | import androidx.compose.ui.unit.dp 7 | 8 | @Composable 9 | fun rememberWindowInfo(): WindowInfo { 10 | val configuration = LocalConfiguration.current 11 | return WindowInfo( 12 | screenWidthInfo = when { 13 | configuration.screenWidthDp < 600 -> WindowInfo.WindowType.Compact 14 | configuration.screenWidthDp < 840 -> WindowInfo.WindowType.Medium 15 | else -> WindowInfo.WindowType.Expanded 16 | }, 17 | screenHeightInfo = when { 18 | configuration.screenHeightDp < 480 -> WindowInfo.WindowType.Compact 19 | configuration.screenHeightDp < 900 -> WindowInfo.WindowType.Medium 20 | else -> WindowInfo.WindowType.Expanded 21 | }, 22 | screenWidth = configuration.screenWidthDp.dp, 23 | screenHeight = configuration.screenHeightDp.dp 24 | ) 25 | } 26 | 27 | data class WindowInfo( 28 | val screenWidthInfo: WindowType, 29 | val screenHeightInfo: WindowType, 30 | val screenWidth: Dp, 31 | val screenHeight: Dp 32 | ) { 33 | sealed class WindowType { 34 | object Compact : WindowType() 35 | object Medium : WindowType() 36 | object Expanded : WindowType() 37 | } 38 | } -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/utils/PreferencesManager.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.utils 2 | 3 | import android.content.Context 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.remember 6 | import androidx.datastore.core.DataStore 7 | import androidx.datastore.preferences.core.MutablePreferences 8 | import androidx.datastore.preferences.core.Preferences 9 | import androidx.datastore.preferences.core.booleanPreferencesKey 10 | import androidx.datastore.preferences.core.edit 11 | import androidx.datastore.preferences.core.floatPreferencesKey 12 | import androidx.datastore.preferences.core.intPreferencesKey 13 | import androidx.datastore.preferences.core.stringPreferencesKey 14 | import androidx.datastore.preferences.core.stringSetPreferencesKey 15 | import androidx.datastore.preferences.preferencesDataStore 16 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 17 | import com.example.androidMic.utils.PreferencesManager.Companion.editor 18 | import kotlinx.coroutines.flow.distinctUntilChanged 19 | import kotlinx.coroutines.flow.first 20 | import kotlinx.coroutines.flow.map 21 | import kotlinx.coroutines.runBlocking 22 | 23 | abstract class PreferencesManager(private val context: Context, name: String) { 24 | private val Context.dataStore: DataStore by preferencesDataStore(name = name) 25 | protected val dataStore get() = context.dataStore 26 | 27 | suspend fun preload() { 28 | dataStore.data.first() 29 | } 30 | 31 | protected fun stringPreference(key: String, default: String = "") = 32 | StringPreference(dataStore, key, default) 33 | 34 | protected fun booleanPreference(key: String, default: Boolean) = 35 | BooleanPreference(dataStore, key, default) 36 | 37 | protected fun intPreference(key: String, default: Int) = IntPreference(dataStore, key, default) 38 | 39 | protected fun floatPreference(key: String, default: Float) = 40 | FloatPreference(dataStore, key, default) 41 | 42 | 43 | protected fun setPreference(key: String, default: Set) = 44 | SetPreference(dataStore, key, default) 45 | 46 | protected inline fun > enumPreference( 47 | key: String, 48 | default: E 49 | ) = EnumPreference(dataStore, key, default, enumValues()) 50 | 51 | companion object { 52 | suspend inline fun DataStore.editor(crossinline block: EditorContext.() -> Unit) { 53 | edit { 54 | EditorContext(it).run(block) 55 | } 56 | } 57 | } 58 | } 59 | 60 | class EditorContext(private val prefs: MutablePreferences) { 61 | var Preference.value 62 | get() = prefs.run { read() } 63 | set(value) = prefs.run { write(value) } 64 | } 65 | 66 | abstract class Preference( 67 | private val dataStore: DataStore, 68 | protected val default: T 69 | ) { 70 | internal abstract fun Preferences.read(): T 71 | internal abstract fun MutablePreferences.write(value: T) 72 | 73 | private val flow = dataStore.data.map { with(it) { read() } ?: default }.distinctUntilChanged() 74 | 75 | fun getFlow() = flow 76 | 77 | suspend fun get() = flow.first() 78 | fun getBlocking() = runBlocking { get() } 79 | 80 | @Composable 81 | fun getAsState() = flow.collectAsStateWithLifecycle(initialValue = remember { 82 | getBlocking() 83 | }) 84 | 85 | suspend fun update(value: T) = dataStore.editor { 86 | this@Preference.value = value 87 | } 88 | } 89 | 90 | class EnumPreference>( 91 | dataStore: DataStore, 92 | key: String, 93 | default: E, 94 | private val enumValues: Array 95 | ) : Preference(dataStore, default) { 96 | private val key = stringPreferencesKey(key) 97 | override fun Preferences.read() = 98 | this[key]?.let { name -> 99 | enumValues.find { it.name == name } 100 | } ?: default 101 | 102 | override fun MutablePreferences.write(value: E) { 103 | this[key] = value.name 104 | } 105 | } 106 | 107 | abstract class BasePreference(dataStore: DataStore, default: T) : 108 | Preference(dataStore, default) { 109 | protected abstract val key: Preferences.Key 110 | override fun Preferences.read() = this[key] ?: default 111 | override fun MutablePreferences.write(value: T) { 112 | this[key] = value 113 | } 114 | } 115 | 116 | class StringPreference( 117 | dataStore: DataStore, 118 | key: String, 119 | default: String 120 | ) : BasePreference(dataStore, default) { 121 | override val key = stringPreferencesKey(key) 122 | } 123 | 124 | class BooleanPreference( 125 | dataStore: DataStore, 126 | key: String, 127 | default: Boolean 128 | ) : BasePreference(dataStore, default) { 129 | override val key = booleanPreferencesKey(key) 130 | } 131 | 132 | class IntPreference( 133 | dataStore: DataStore, 134 | key: String, 135 | default: Int 136 | ) : BasePreference(dataStore, default) { 137 | override val key = intPreferencesKey(key) 138 | } 139 | 140 | class FloatPreference( 141 | dataStore: DataStore, 142 | key: String, 143 | default: Float 144 | ) : BasePreference(dataStore, default) { 145 | override val key = floatPreferencesKey(key) 146 | } 147 | 148 | class SetPreference( 149 | dataStore: DataStore, 150 | key: String, 151 | default: Set 152 | ) : BasePreference>(dataStore, default) { 153 | override val key = stringSetPreferencesKey(key) 154 | } 155 | 156 | 157 | -------------------------------------------------------------------------------- /Android/app/src/main/java/com/example/androidMic/utils/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.example.androidMic.utils 2 | 3 | import java.net.InetSocketAddress 4 | 5 | // helper function to ignore some exceptions 6 | inline fun ignore(body: () -> Unit) { 7 | try { 8 | body() 9 | } catch (e: Exception) { 10 | e.printStackTrace() 11 | } 12 | } 13 | 14 | 15 | fun checkIp(ip: String): Boolean { 16 | return try { 17 | InetSocketAddress(ip, 6000) 18 | true 19 | } catch (e: Exception) { 20 | false 21 | } 22 | } 23 | 24 | fun checkPort(portStr: String): Boolean { 25 | val port = try { 26 | portStr.toInt() 27 | } catch (e: NumberFormatException) { 28 | return false 29 | } 30 | return try { 31 | InetSocketAddress("127.0.0.1", port) 32 | true 33 | } catch (e: Exception) { 34 | false 35 | } 36 | } 37 | 38 | fun Int.toBigEndianU32(): ByteArray { 39 | val unsigned = this.toLong() and 0xFFFFFFFFL 40 | 41 | val bytes = ByteArray(4) 42 | for (i in 0 until 4) { 43 | bytes[i] = (unsigned shr (24 - i * 8) and 0xFF).toByte() 44 | } 45 | 46 | return bytes 47 | } 48 | 49 | fun ByteArray.chunked(size: Int): List { 50 | if (size <= 0) throw IllegalArgumentException("Size must be greater than 0") 51 | return (0 until size step size).map { start -> 52 | copyOfRange( 53 | start, (start + size).coerceAtMost( 54 | size 55 | ) 56 | ) 57 | } 58 | } -------------------------------------------------------------------------------- /Android/app/src/main/proto/message.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message AudioPacketMessage { 4 | bytes buffer = 1; 5 | uint32 sample_rate = 2; 6 | uint32 channel_count = 3; 7 | uint32 audio_format = 4; 8 | } 9 | 10 | message AudioPacketMessageOrdered { 11 | uint32 sequence_number = 1; 12 | AudioPacketMessage audio_packet = 2; 13 | } -------------------------------------------------------------------------------- /Android/app/src/main/res/drawable-v24/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 15 | 16 | 17 | 23 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Android/app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 15 | 18 | 21 | 24 | 27 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Android/app/src/main/res/drawable/ic_launcher_monochrome.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 17 | 20 | 23 | 26 | 27 | -------------------------------------------------------------------------------- /Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclouday/AndroidMic/0b02cce1250e75f1bb5b2499973722f2263849a1/Android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /Android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclouday/AndroidMic/0b02cce1250e75f1bb5b2499973722f2263849a1/Android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclouday/AndroidMic/0b02cce1250e75f1bb5b2499973722f2263849a1/Android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /Android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclouday/AndroidMic/0b02cce1250e75f1bb5b2499973722f2263849a1/Android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclouday/AndroidMic/0b02cce1250e75f1bb5b2499973722f2263849a1/Android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /Android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclouday/AndroidMic/0b02cce1250e75f1bb5b2499973722f2263849a1/Android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclouday/AndroidMic/0b02cce1250e75f1bb5b2499973722f2263849a1/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclouday/AndroidMic/0b02cce1250e75f1bb5b2499973722f2263849a1/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclouday/AndroidMic/0b02cce1250e75f1bb5b2499973722f2263849a1/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamclouday/AndroidMic/0b02cce1250e75f1bb5b2499973722f2263849a1/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Android/app/src/main/res/values-fr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | Connecter 6 | Deconnecter 7 | Enregister l\'audio 8 | 9 | 12 | Paramètre 13 | Connection 14 | Autre 15 | 16 | Mode de connection 17 | Thèmes 18 | Couleur dynamique 19 | 20 | Réinitialiser 21 | Enregistrer 22 | Annuler 23 | 24 | Mono 25 | Stéréo 26 | 27 | 28 | 31 | IP:PORT invalide 32 | PORT invalide 33 | 34 | Erreur\n 35 | Echec de connection 36 | Le stream a déjà commencé 37 | Le microphone enregistre déjà 38 | 39 | Le microphone n\'enregistre pas 40 | Appareil connecté:\n 41 | Le microphone a commencé à enregister 42 | 43 | Appareil déconnecté 44 | Arrêt de l\'enregistrement 45 | 46 | 47 | 50 | Le stream audio est actif 51 | 52 | Début stream 53 | Début enregistrement 54 | Arrêt stream 55 | Arrêt enregistrement 56 | 57 | -------------------------------------------------------------------------------- /Android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AndroidMic 3 | 4 | 7 | Connect 8 | Disconnect 9 | Record Audio 10 | 11 | 14 | Setting 15 | Connection 16 | Record 17 | Other 18 | 19 | 20 | Connection mode 21 | 22 | IP:PORT 23 | IP 24 | PORT 25 | 26 | 27 | Sample Rate 28 | Channel Count 29 | Audio Format 30 | 31 | Themes 32 | Dynamic color 33 | 34 | Reset 35 | Save 36 | Cancel 37 | 38 | Mono 39 | Stereo 40 | 41 | 44 | IP:PORT invalid 45 | PORT invalid 46 | 47 | Error\n 48 | Failed to connect 49 | Stream already started 50 | Microphone already started 51 | Microphone is not recording 52 | 53 | Connected Device:\n 54 | Microphone has started to record 55 | 56 | Device disconnected 57 | Recording stopped 58 | 59 | 62 | Audio stream is active 63 | 64 | Start streaming 65 | Start recording 66 | Stop streaming 67 | Stop recording 68 | 69 | -------------------------------------------------------------------------------- /Android/app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |