commit 0825833a55bf23cbf26c8f229375edb148ef5269 Author: cjones Date: Mon Jul 13 16:11:49 2026 -0400 First commit of Whisper client diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..7643783 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,123 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..ca16a99 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..02c4aa5 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..7061a0d --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,61 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..74dd639 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/studiobot.xml b/.idea/studiobot.xml new file mode 100644 index 0000000..539e3b8 --- /dev/null +++ b/.idea/studiobot.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..aa74583 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,60 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.compose) + kotlin("plugin.serialization") version libs.versions.kotlin.get() apply false +} + +android { + namespace = "com.objectbrokers.whisperclient2" + compileSdk { + version = release(37) { + minorApiLevel = 0 + } + } + + defaultConfig { + applicationId = "com.objectbrokers.whisperclient2" + minSdk = 24 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + optimization { + enable = false + } + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + buildFeatures { + compose = true + } +} + +dependencies { + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation("com.squareup.okhttp3:okhttp:5.4.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") + testImplementation(libs.junit) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.compose.ui.test.junit4) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(libs.androidx.junit) + debugImplementation(libs.androidx.compose.ui.test.manifest) + debugImplementation(libs.androidx.compose.ui.tooling) +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/objectbrokers/whisperclient2/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/objectbrokers/whisperclient2/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..2504934 --- /dev/null +++ b/app/src/androidTest/java/com/objectbrokers/whisperclient2/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.objectbrokers.whisperclient2 + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.objectbrokers.whisperclient2", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..39ffc0b --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/objectbrokers/whisperclient2/MainActivity.kt b/app/src/main/java/com/objectbrokers/whisperclient2/MainActivity.kt new file mode 100644 index 0000000..8b2a205 --- /dev/null +++ b/app/src/main/java/com/objectbrokers/whisperclient2/MainActivity.kt @@ -0,0 +1,411 @@ +package com.objectbrokers.whisperclient2 + +import android.Manifest +import android.annotation.SuppressLint +import android.content.pm.PackageManager +import android.media.AudioFormat +import android.media.AudioRecord +import android.media.MediaRecorder +import android.os.Bundle +import android.util.Log +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope + +//import androidx.privacysandbox.tools.core.generator.build +import kotlinx.coroutines.Job +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import okhttp3.WebSocket +import okio.ByteString +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.WebSocketListener +import com.objectbrokers.whisperclient2.ui.theme.WhisperClient2Theme +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext +import okio.ByteString.Companion.toByteString +import org.json.JSONObject +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.util.UUID +import kotlin.math.roundToInt + +const val TAG = "SpeechClient" + +class MainActivity : ComponentActivity() { + + private var recordingJob: Job? = null + private var audioCaptureJob: Job? = null +// private var isActive: Boolean = false + private lateinit var audioRecord: AudioRecord + private var webSocket: WebSocket? = null + private var clientUid: UUID? = null + private var bufferSize: Int = 0 + private var buffer: ByteArray? = null + private var isRecording by mutableStateOf(false) + private var transcriptText by mutableStateOf("Transcription will appear here...") + private val client = OkHttpClient() + private val requestPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted: Boolean -> + if (isGranted) { + // Permission granted! You can now call startRecording() + } else { + // Permission denied. Show a message to the user. + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + WhisperClient2Theme { + WhisperApp(transcriptText = transcriptText, + isRecording = isRecording) { + // this block IS the onToggleRecording lambda + if (isRecording) + { + stopRecording() + isRecording = false + } + else + { + startRecording(); isRecording = true + } + } + } + } + + when { + ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == + PackageManager.PERMISSION_GRANTED -> { + // Already have permission + } + else -> { + // Ask for it + requestPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO) + } + } + } + + class AudioInitializationException(message: String) : Exception(message) + + @SuppressLint("MissingPermission") // Ensure runtime check is completed beforehand + fun createAudioRecorder(): AudioRecord { + // 1. Get the minimum buffer size required for the hardware configuration + val minBufferSize = AudioRecord.getMinBufferSize( + 16000, + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT + ) + + // 2. Build and return the AudioRecord instance + var recorder: AudioRecord? = null; + recorder = AudioRecord.Builder() + .setAudioSource(MediaRecorder.AudioSource.MIC) + .setAudioFormat( + AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_PCM_16BIT) + .setSampleRate(16000) + .setChannelMask(AudioFormat.CHANNEL_IN_MONO) + .build() + ) + // Double or triple the min buffer size to prevent buffer overflow (over-runs) + .setBufferSizeInBytes(minBufferSize * 2) + .build() + + if (recorder.state != AudioRecord.STATE_INITIALIZED) { + recorder.release() + throw AudioInitializationException("AudioRecord failed to initialize — mic unavailable or permission denied") + } + return recorder + } + + fun startRecording() { + recordingJob = lifecycleScope.launch(Dispatchers.IO) { + var audioRecordOK = true + bufferSize = AudioRecord.getMinBufferSize( + 16000, + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT + ) * 2 + + buffer = ByteArray(bufferSize) + // Create the AudioRecord instance + try { + audioRecord = createAudioRecorder() + } catch (e: AudioInitializationException) { + Log.e(TAG, "Audio init failed: ${e.message}") + withContext(Dispatchers.Main) { + Toast.makeText(this@MainActivity, "Audio init failed: ${e.message}", Toast.LENGTH_LONG).show() + audioRecordOK = false + } + } + if (audioRecordOK) { + // Create the WebSocket instance + val request = Request.Builder() + .url("ws://100.81.165.11:9097") + .build() + val listener = object : WebSocketListener() { + override fun onOpen(webSocket: WebSocket, response: Response) { + // get a new client UUID (unique session ID) for each connection + clientUid = UUID.randomUUID() + // Build the handshake JSON using Android's built-in JSONObject. + // Every field the server expects is here; safe to hardcode the ones + // you don't need to expose in the UI. + val handshake = JSONObject().apply { + put("uid", clientUid.toString()) + put("task", "transcribe") + put("model", "OpenVINO/whisper-medium-fp16-ov") + put("language", "en") + put("use_vad", false) + put("audio_format", "int16") + put("send_last_n_segments", 10) + put("no_speech_thresh", 0.45) + put("clip_audio", false) + put("same_output_threshold", 10) + put("enable_translation", false) + put( + "target_language", + "fr" + ) // server requires this key even if translation is off + put("hotwords", JSONObject.NULL) + put("enable_diarization", false) + put("max_speakers", 10) + put("word_timestamps", false) + } + // send() with a String argument sends a WebSocket text frame + webSocket.send(handshake.toString()) + } + + // Handle onClosing, onFailure, etc. + + override fun onMessage(webSocket: WebSocket, text: String) { + Log.i(TAG, "Received message: $text") + val message = JSONObject(text) + + // 1. Validate this message is meant for us. + // Store the uid you sent in the handshake as a class member. + if (message.optString("uid") != clientUid.toString()) { + Log.e(TAG, "Received message with wrong uid — ignoring") + return + } + + // 2. Status messages — server full, error, or warning. + // These arrive at any time, not just after the handshake. + if (message.has("status")) { + when (message.getString("status")) { + "WAIT" -> { + val minutes = message.getDouble("message").roundToInt() + Log.i(TAG, "Server full. Estimated wait: $minutes minutes") + // Optionally show this in the UI + } + + "ERROR" -> { + Log.e(TAG, "Server error: ${message.getString("message")}") + // Stop recording, show error in UI + } + + "WARNING" -> { + Log.w(TAG, "Server warning: ${message.getString("message")}") + } + } + return + } + + // 3. Control messages — check the "message" key for named signals. + if (message.has("message")) { + when (message.getString("message")) { + + "SERVER_READY" -> { + // This is the handshake acknowledgement you asked about. + // Only start sending audio after receiving this. + val backend = + message.getString("backend") // e.g. "faster_whisper" + Log.i(TAG, "Server ready, backend: $backend") + runOnUiThread { +// statusTextView.text = "Connected" + } + startAudioCapture() // <-- safe to start sending audio now + } + + "DISCONNECT" -> { + // Server hung up because the session exceeded MAX_CONNECTION_TIME + Log.i(TAG, "Server disconnected: session time limit reached") + stopRecording() + } + } + return + } + + // 4. Language detection — server tells us what language it detected. + // Only relevant if you sent language=null in the handshake. + if (message.has("language")) { + val lang = message.getString("language") + val prob = message.getDouble("language_prob") + Log.i(TAG, "Detected language: $lang (probability: $prob)") + return + } + + // 5. Transcript segments — the normal steady-state message during recording. + if (message.has("segments")) { + val segments = message.getJSONArray("segments") + val sb = StringBuilder() + for (i in 0 until segments.length()) { + sb.append(segments.getJSONObject(i).getString("text").trim()) + if (i < segments.length() - 1) sb.append(" ") + } + Log.i(TAG, "Transcript: $sb") + runOnUiThread { + val text12: String = sb.toString() + this@MainActivity.transcriptText = text12 +// transcriptTextView.text = transcriptText + } + } + } + + override fun onFailure( + webSocket: WebSocket, + t: Throwable, + response: Response? + ) { + Log.e(TAG, "WebSocket connection failed: ${t.message}", t) + } + } + + webSocket = client.newWebSocket(request, listener) + Log.i(TAG, "WebSocket: $webSocket") + } + } + } + + + fun stopRecording() { + stopAudioCapture() + recordingJob?.cancel() // the ?. is Kotlin null-safety — no NPE if job is null + webSocket?.close(1000, "Done") + webSocket = null + } + + fun stopAudioCapture() { + audioCaptureJob?.cancel() + audioRecord.stop() + audioRecord.release() + } + + fun startAudioCapture() { + audioCaptureJob = lifecycleScope.launch(Dispatchers.IO) { + audioRecord.startRecording(); + Log.i(TAG, "audioRecord.state: ${audioRecord.state}") + while (isActive) { + buffer?.let { + val bytesRead = audioRecord.read(it, 0, bufferSize) + Log.i(TAG, "bytesRead: $bytesRead") + if (bytesRead > 0) { + // Check RMS energy — if this is always near zero, AudioRecord + // is returning silence regardless of what the mic hears + val shorts = ShortArray(bytesRead / 2) + ByteBuffer.wrap(it, 0, bytesRead) + .order(ByteOrder.LITTLE_ENDIAN) + .asShortBuffer() + .get(shorts) + val rms = Math.sqrt(shorts.map { s -> s.toDouble() * s }.average()) + Log.d(TAG, "Audio chunk RMS: $rms") + webSocket?.send(it.toByteString(0, bytesRead)) + } + } + } + } + } + + fun sendAudioPacket(webSocket: WebSocket, audioBytes: ByteArray) { + // OkHttp's send() is overloaded: + // send(String) → text frame + // send(ByteString) → binary frame ← this is what we want + // ByteString.of() wraps a ByteArray without copying if possible. + webSocket.send(ByteString.of(*audioBytes)) + } +} + +/* +@Composable +fun WhisperApp(text: String) +{ // ... inside your scrollable column ... Text( text = text, +// It is now tied to the 'transcriptText' variable style = MaterialTheme.typography.bodyLarge ) } + */ +@Composable +fun WhisperApp( + transcriptText: String, + isRecording: Boolean = false, + onToggleRecording: () -> Unit = {} +) { + Scaffold( + modifier = Modifier.fillMaxSize() + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = transcriptText, + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 16.dp) + ) + + Column( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + ) { + Text( + text = "Transcription will appear here...", + style = MaterialTheme.typography.bodyLarge + ) + } + + Button( + onClick = onToggleRecording, + modifier = Modifier.fillMaxWidth().padding(top = 16.dp), + content = { Text(if (isRecording) "Stop Transcription" else "Start Transcription") } + ) + } + } +} + +@Preview(showBackground = true) +@Composable +fun WhisperAppPreview() { + WhisperClient2Theme { + WhisperApp("Transcription will appear here...") + } +} diff --git a/app/src/main/java/com/objectbrokers/whisperclient2/scratch.txt b/app/src/main/java/com/objectbrokers/whisperclient2/scratch.txt new file mode 100644 index 0000000..8524771 --- /dev/null +++ b/app/src/main/java/com/objectbrokers/whisperclient2/scratch.txt @@ -0,0 +1,4 @@ +u share: +The exact model of your CPU/GPU (e.g., Intel Core i7-13700H, Intel Arc A770)? +The output of running ls -l /dev/dri on your host machine? +2 sitesHow to fix the Docker permission denied error?Mar 10, 2026 — 1. Add your user to the Docker group The most common cause of the permission denied error is that your user account isn't part of ...HostingerHow to Deploy a Hugging Face Model on a GPU-Powered Docker ContainerMay 23, 2025 — Set environment variables to toggle between CPU/GPU or enable debuggingRunpod \ No newline at end of file diff --git a/app/src/main/java/com/objectbrokers/whisperclient2/ui/theme/Color.kt b/app/src/main/java/com/objectbrokers/whisperclient2/ui/theme/Color.kt new file mode 100644 index 0000000..b6e83f0 --- /dev/null +++ b/app/src/main/java/com/objectbrokers/whisperclient2/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.objectbrokers.whisperclient2.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/com/objectbrokers/whisperclient2/ui/theme/Theme.kt b/app/src/main/java/com/objectbrokers/whisperclient2/ui/theme/Theme.kt new file mode 100644 index 0000000..c97d0f8 --- /dev/null +++ b/app/src/main/java/com/objectbrokers/whisperclient2/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package com.objectbrokers.whisperclient2.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun WhisperClient2Theme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/objectbrokers/whisperclient2/ui/theme/Type.kt b/app/src/main/java/com/objectbrokers/whisperclient2/ui/theme/Type.kt new file mode 100644 index 0000000..453a43e --- /dev/null +++ b/app/src/main/java/com/objectbrokers/whisperclient2/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.objectbrokers.whisperclient2.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/app/src/main/keepRules/rules.keep b/app/src/main/keepRules/rules.keep new file mode 100644 index 0000000..d7e081a --- /dev/null +++ b/app/src/main/keepRules/rules.keep @@ -0,0 +1,12 @@ +# Add project specific R8 rules here. +# AGP will combine all keep rule files in src/main/keepRules to pass to R8 +# +# For more details, see +# https://d.android.com/r/tools/r8/keep-rules + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..1e11ccf --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + WhisperClient2 + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..721d04b --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +