Added .gitignore and updated to include the USB connection and stream on certainty of word/phrase results....

This commit is contained in:
2026-01-22 16:32:36 +11:00
commit 813441645c
43 changed files with 1068 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
package net.mmanningau.speechtokeyboard
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("net.mmanningau.speechtokeyboard", appContext.packageName)
}
}

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-feature android:name="android.hardware.usb.host" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.SpeechToKeyboard"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
</intent-filter>
<meta-data
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/device_filter" />
</activity>
<activity
android:name=".TestModelActivity"
android:parentActivityName=".MainActivity"
android:label="Test Microphone" />
</application>
</manifest>

View File

@@ -0,0 +1,149 @@
package net.mmanningau.speechtokeyboard // <--- MAKE SURE THIS MATCHES YOUR PACKAGE NAME
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import org.vosk.Model
import org.vosk.android.SpeechService
import java.io.File
import java.util.zip.ZipInputStream
class MainActivity : AppCompatActivity() {
// UI Components
private lateinit var statusText: TextView
// Vosk Components
private var model: Model? = null
private var speechService: SpeechService? = null
// 1. THE FILE PICKER REGISTRY
// This handles the result when the user picks a ZIP file
private val pickZipFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let { uri ->
// User picked a file. Now we install it.
installModelFromUri(uri)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
statusText = findViewById(R.id.status_text_view) // Make sure you have a TextView with this ID in your layout
statusText.text = "Checking for existing model...."
// ADD THIS LINE AT THE BOTTOM:
// This attempts to load the model immediately if files exist
initVoskModel()
}
// 2. SETUP THE MENU
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.main_menu, menu)
return true
}
// 3. HANDLE MENU CLICKS
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_load_model -> {
openFilePicker()
true
}
R.id.action_test_model -> {
// Launch the new Test Activity
val intent = Intent(this, TestModelActivity::class.java)
startActivity(intent)
true
}
else -> super.onOptionsItemSelected(item)
}
}
// 4. OPEN SYSTEM FILE PICKER
private fun openFilePicker() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/zip" // Only show ZIP files
}
pickZipFile.launch(intent)
}
// 5. INSTALLATION LOGIC (Unzip to private storage)
private fun installModelFromUri(uri: android.net.Uri) {
statusText.text = "Installing model... please wait."
// Run in background thread to avoid freezing UI
Thread {
try {
contentResolver.openInputStream(uri)?.use { inputStream ->
val zipInputStream = ZipInputStream(inputStream)
val targetDir = File(filesDir, "vosk-model")
// Clean up old model if exists
if (targetDir.exists()) targetDir.deleteRecursively()
targetDir.mkdirs()
// Unzip loop
var entry = zipInputStream.nextEntry
while (entry != null) {
val outFile = File(targetDir, entry.name)
if (entry.isDirectory) {
outFile.mkdirs()
} else {
// Ensure parent dir exists
outFile.parentFile?.mkdirs()
outFile.outputStream().use { output ->
zipInputStream.copyTo(output)
}
}
entry = zipInputStream.nextEntry
}
}
// Back to UI Thread to say success
runOnUiThread {
statusText.text = "Model Installed! Initializing..."
initVoskModel()
}
} catch (e: Exception) {
runOnUiThread {
Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_LONG).show()
statusText.text = "Installation Failed."
}
}
}.start()
}
// 6. INITIALIZE VOSK "BRAIN"
// Replace your existing initVoskModel with this updated version
private fun initVoskModel() {
val modelPath = File(filesDir, "vosk-model")
// Check if the directory exists before trying to load
if (!modelPath.exists()) {
statusText.text = "No model found. Please load one."
return
}
val actualModelDir = modelPath.listFiles()?.firstOrNull { it.isDirectory } ?: modelPath
try {
model = Model(actualModelDir.absolutePath)
statusText.text = "Model Loaded & Ready!"
// Optional: Enable your 'Test' button here if you disabled it
} catch (e: Exception) {
statusText.text = "Error loading saved model: ${e.message}"
}
}
}

View File

@@ -0,0 +1,254 @@
package net.mmanningau.speechtokeyboard
import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.widget.ImageButton
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import org.json.JSONObject
import org.vosk.Model
import org.vosk.Recognizer
import org.vosk.android.RecognitionListener
import org.vosk.android.SpeechService
import java.io.File
import android.content.Context
import android.hardware.usb.UsbManager
import com.hoho.android.usbserial.driver.UsbSerialPort
import com.hoho.android.usbserial.driver.UsbSerialProber
import com.hoho.android.usbserial.util.SerialInputOutputManager
class TestModelActivity : AppCompatActivity(), RecognitionListener {
private lateinit var outputText: TextView
private lateinit var micButton: ImageButton
// Vosk Components
private var model: Model? = null
private var speechService: SpeechService? = null
private var isListening = false
// USB Components
private var usbPort: UsbSerialPort? = null
private var usbIoManager: SerialInputOutputManager? = null // Handles the data flow
private var committedText = "" // Stores the finalized sentences
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test_model)
// Bind UI
outputText = findViewById(R.id.text_output_log)
micButton = findViewById(R.id.btn_mic_toggle)
// Check Permissions immediately
checkAudioPermission()
// Setup Button Listener
micButton.setOnClickListener {
toggleListening()
}
private fun connectToPico() {
val usbManager = getSystemService(Context.USB_SERVICE) as UsbManager
// 1. Find the Device
// (This probes specifically for devices listed in your device_filter.xml)
val availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(usbManager)
if (availableDrivers.isEmpty()) {
outputText.append("\n> No USB device found.")
return
}
// Assume the first device found is the Pico
val driver = availableDrivers[0]
val connection = usbManager.openDevice(driver.device)
if (connection == null) {
outputText.append("\n> Permission denied. Re-plug device?")
return
}
// 2. Open the Port
// Most Picos use port 0.
usbPort = driver.ports[0]
try {
usbPort?.open(connection)
// 3. Set Parameters (Must match your Pico's C/Python code!)
// 115200 Baud, 8 Data bits, 1 Stop bit, No Parity
usbPort?.setParameters(115200, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE)
outputText.append("\n> USB Connected to Pico!")
} catch (e: Exception) {
outputText.append("\n> USB Error: ${e.message}")
}
}
// Initialize the model in background
initModel()
}
private fun initModel() {
// We look for the folder inside private storage (same logic as MainActivity)
val modelPath = File(filesDir, "vosk-model")
if (!modelPath.exists()) {
outputText.text = "Error: Model not found. Please go back and load a model first."
micButton.isEnabled = false
return
}
Thread {
try {
// Find the actual model folder inside
val actualModelDir = modelPath.listFiles()?.firstOrNull { it.isDirectory } ?: modelPath
model = Model(actualModelDir.absolutePath)
runOnUiThread {
outputText.append("\n\n> Model Loaded. Ready.")
}
} catch (e: Exception) {
runOnUiThread {
outputText.text = "Error loading model: ${e.message}"
}
}
}.start()
}
private fun sendToPico(text: String) {
if (usbPort == null) return // Safety check
try {
// Convert text to bytes and send
val data = text.toByteArray(Charsets.UTF_8)
usbPort?.write(data, 1000) // 1000ms timeout
} catch (e: Exception) {
outputText.append("\n[Send Failed: ${e.message}]")
}
}
private fun toggleListening() {
if (model == null) {
Toast.makeText(this, "Model not loaded yet", Toast.LENGTH_SHORT).show()
return
}
if (isListening) {
stopRecognition()
} else {
startRecognition()
}
}
private fun startRecognition() {
try {
val recognizer = Recognizer(model, 16000.0f) // 16kHz is standard for Vosk
speechService = SpeechService(recognizer, 16000.0f)
//speechService?.addListener(this) <----- removed this as it generated an error
speechService?.startListening(this)
isListening = true
micButton.setColorFilter(android.graphics.Color.RED) // Turn button red
outputText.text = "" // Clear previous text
outputText.append("> Listening...\n")
} catch (e: Exception) {
outputText.append("\nError starting mic: ${e.message}")
}
}
private fun stopRecognition() {
speechService?.stop()
speechService = null
isListening = false
micButton.clearColorFilter() // Reset button color
outputText.append("\n> Stopped.")
}
// --- Vosk Listener Callbacks ---
override fun onResult(hypothesis: String?) {
hypothesis?.let {
val text = parseVoskResult(it)
if (text.isNotEmpty()) {
// 1. Update the UI History
// Add the new sentence to our history
committedText += "$text. "
// Update screen
outputText.text = "$committedText"
// 2. SEND TO PICO
// We append a space because speech engines strip trailing spaces,
// and you don't want "helloworld" typed into your computer.
sendToPico("$text ")
}
}
}
override fun onPartialResult(hypothesis: String?) {
// Optional: Shows words as they are being spoken (streaming)
// You can enable this if you want to see "typing" effect
hypothesis?.let {
// Parse the "partial" JSON key
val partial = JSONObject(it).optString("partial", "")
if (partial.isNotEmpty()) {
// Display: [History] + [Current Streaming Guess]
outputText.text = "$committedText $partial..."
}
}
}
override fun onFinalResult(hypothesis: String?) {
// Final flush when stopping
hypothesis?.let {
val text = parseVoskResult(it)
if (text.isNotEmpty()) {
outputText.append("$text\n")
}
}
}
override fun onError(exception: Exception?) {
outputText.append("\nError: ${exception?.message}")
}
override fun onTimeout() {
outputText.append("\nTimeout.")
}
// Helper to clean JSON: {"text": "hello world"} -> "hello world"
private fun parseVoskResult(json: String): String {
return try {
JSONObject(json).optString("text", "")
} catch (e: Exception) {
""
}
}
// Permission Helper
private fun checkAudioPermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO), 1)
}
}
// Cleanup on exit
override fun onDestroy() {
super.onDestroy()
speechService?.shutdown()
// Close USB
try {
usbPort?.close()
} catch (e: Exception) {
// Ignore errors on close
}
}
}

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="20dp">
<TextView
android:id="@+id/status_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Status"
android:textSize="18sp" />
</LinearLayout>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:weightSum="4">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="#EEEEEE">
<ImageButton
android:id="@+id/btn_mic_toggle"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_gravity="center"
android:background="@android:drawable/btn_default"
android:src="@android:drawable/ic_btn_speak_now"
android:scaleType="fitCenter"
android:padding="10dp"
android:contentDescription="Toggle Microphone" />
</FrameLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="3"
android:fillViewport="true"
android:padding="16dp">
<TextView
android:id="@+id/text_output_log"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Press the mic to start listening..."
android:textSize="18sp"
android:fontFamily="monospace"
android:textColor="#000000" />
</ScrollView>
</LinearLayout>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_load_model"
android:icon="@android:drawable/ic_menu_manage"
android:title="Load Vosk Model"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_test_model"
android:icon="@android:drawable/ic_btn_speak_now"
android:title="Test Model"
app:showAsAction="ifRoom" />
</menu>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.SpeechToKeyboard" parent="Theme.Material3.DayNight">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
</style>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Speech To Keyboard</string>
</resources>

View File

@@ -0,0 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.SpeechToKeyboard" parent="Theme.Material3.DayNight">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
<style name="Theme.SpeechToKeyboard" parent="Base.Theme.SpeechToKeyboard" />
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<usb-device vendor-id="11914" />
</resources>

View File

@@ -0,0 +1,17 @@
package net.mmanningau.speechtokeyboard
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}