Initial commit, adding original binaries, initial watch and companion apps which can talk
@@ -0,0 +1,24 @@
|
||||
package de.ladkau.schwertundmagieonpebblecompanionapp
|
||||
|
||||
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("de.ladkau.schwertundmagieonpebblecompanionapp", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?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.INTERNET" />
|
||||
|
||||
<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.SchwertUndMagieOnPebbleCompanionApp">
|
||||
<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>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,37 @@
|
||||
package de.ladkau.schwertundmagieonpebblecompanionapp
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
class C64Engine {
|
||||
// Native hooks representing underlying open-source emulator loops (e.g., Frodo Core)
|
||||
external fun initEmulator(romPath: String): Boolean
|
||||
external fun runFrame()
|
||||
external fun getRawVideoBuffer(buffer: ByteBuffer) // Populates native 320x200 ARGB pixels
|
||||
external fun injectNativeKey(keyCode: Int, isPressed: Boolean)
|
||||
|
||||
companion object {
|
||||
init {
|
||||
System.loadLibrary("frodoc64_core")
|
||||
}
|
||||
}
|
||||
|
||||
// Maps standard incoming string commands to traditional C64 matrix keycodes
|
||||
fun handleWatchInput(command: String) {
|
||||
val c64KeyCode = when (command) {
|
||||
"1" -> 0x31 // C64 '1'
|
||||
"2" -> 0x32 // C64 '2'
|
||||
"3" -> 0x33 // C64 '3'
|
||||
"Y" -> 0x59 // C64 'Y'
|
||||
"N" -> 0x4E // C64 'N'
|
||||
else -> return
|
||||
}
|
||||
|
||||
// Simulates a clean hardware keypress cycle
|
||||
Thread {
|
||||
injectNativeKey(c64KeyCode, true)
|
||||
Thread.sleep(100) // Hold down frame duration
|
||||
injectNativeKey(c64KeyCode, false)
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package de.ladkau.schwertundmagieonpebblecompanionapp
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import com.getpebble.android.kit.PebbleKit
|
||||
import com.getpebble.android.kit.PebbleKit.PebbleDataReceiver
|
||||
import com.getpebble.android.kit.util.PebbleDictionary
|
||||
import java.util.UUID
|
||||
|
||||
class EmulatorService : Service() {
|
||||
private val PEBBLE_APP_UUID = UUID.fromString("6d616769-652d-c64p-ebbl-e57591684000")
|
||||
private val KEY_COMMAND = 1
|
||||
|
||||
private lateinit var emulatorEngine: C64Engine
|
||||
private lateinit var watchStreamer: PebbleStreamer
|
||||
private var isRunning = false
|
||||
private var pebbleDataReceiver: PebbleDataReceiver? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
emulatorEngine = C64Engine()
|
||||
watchStreamer = PebbleStreamer(this)
|
||||
|
||||
// Boot standard disk path containing Schwert und Magie Part 1
|
||||
val romPath = filesDir.absolutePath + "/schwert_und_magie_1.d64"
|
||||
emulatorEngine.initEmulator(romPath)
|
||||
|
||||
setupPebbleInputReceiver()
|
||||
startEmulationLoop()
|
||||
}
|
||||
|
||||
private fun setupPebbleInputReceiver() {
|
||||
pebbleDataReceiver = object : PebbleDataReceiver(PEBBLE_APP_UUID) {
|
||||
override fun receiveData(context: Context?, transactionId: Int, data: PebbleDictionary) {
|
||||
// Read ASCII string parsed by watch selections
|
||||
val command = data.getString(KEY_COMMAND)
|
||||
if (command != null) {
|
||||
emulatorEngine.handleWatchInput(command)
|
||||
}
|
||||
}
|
||||
}
|
||||
PebbleKit.registerReceivedDataHandler(this, pebbleDataReceiver)
|
||||
}
|
||||
|
||||
private fun startEmulationLoop() {
|
||||
isRunning = true
|
||||
Thread {
|
||||
val TargetFrameTimeMs = 33 // Aiming roughly around standard video refresh increments
|
||||
while (isRunning) {
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
emulatorEngine.runFrame()
|
||||
watchStreamer.processAndStreamFrame(emulatorEngine)
|
||||
|
||||
val elapsed = System.currentTimeMillis() - startTime
|
||||
if (elapsed < TargetFrameTimeMs) {
|
||||
Thread.sleep(TargetFrameTimeMs - elapsed)
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
isRunning = false
|
||||
super.onDestroy()
|
||||
try {
|
||||
unregisterReceiver(pebbleDataReceiver)
|
||||
} catch (e: Exception) { /* Context safety cleanup */ }
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package de.ladkau.schwertundmagieonpebblecompanionapp
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.widget.ScrollView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import fi.iki.elonen.NanoHTTPD
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
private const val PORT = 8888
|
||||
private const val TAG = "SuM"
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var tvStatus: TextView
|
||||
private lateinit var tvTime: TextView
|
||||
private lateinit var tvLog: TextView
|
||||
private lateinit var scrollLog: ScrollView
|
||||
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private val timeFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
|
||||
private var server: CompanionServer? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
tvStatus = findViewById(R.id.tv_status)
|
||||
tvTime = findViewById(R.id.tv_time)
|
||||
tvLog = findViewById(R.id.tv_log)
|
||||
scrollLog = findViewById(R.id.scroll_log)
|
||||
|
||||
server = CompanionServer(PORT, timeFormat,
|
||||
onTimeFetched = { time ->
|
||||
mainHandler.post { tvTime.text = "Serving time: $time" }
|
||||
},
|
||||
onKey = { cmd ->
|
||||
Log.d(TAG, "Key received: $cmd")
|
||||
mainHandler.post { appendLog(cmd) }
|
||||
}
|
||||
)
|
||||
try {
|
||||
server?.start()
|
||||
tvStatus.text = "Listening on localhost:$PORT"
|
||||
Log.d(TAG, "HTTP server started on port $PORT")
|
||||
} catch (e: Exception) {
|
||||
tvStatus.text = "Server error: ${e.message}"
|
||||
Log.e(TAG, "Failed to start server", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun appendLog(entry: String) {
|
||||
val timestamp = timeFormat.format(Date())
|
||||
tvLog.text = "[$timestamp] $entry\n${tvLog.text}"
|
||||
scrollLog.post { scrollLog.scrollTo(0, 0) }
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
server?.stop()
|
||||
}
|
||||
}
|
||||
|
||||
class CompanionServer(
|
||||
port: Int,
|
||||
private val timeFormat: SimpleDateFormat,
|
||||
private val onTimeFetched: (String) -> Unit,
|
||||
private val onKey: (String) -> Unit
|
||||
) : NanoHTTPD(port) {
|
||||
|
||||
override fun serve(session: IHTTPSession): Response {
|
||||
Log.d(TAG, "HTTP ${session.method} ${session.uri} params=${session.parameters}")
|
||||
val response = when (session.uri) {
|
||||
"/time" -> {
|
||||
val time = timeFormat.format(Date())
|
||||
onTimeFetched(time)
|
||||
newFixedLengthResponse(
|
||||
Response.Status.OK,
|
||||
"application/json",
|
||||
"""{"time":"$time"}"""
|
||||
)
|
||||
}
|
||||
"/key" -> {
|
||||
val cmd = session.parameters["cmd"]?.firstOrNull().orEmpty()
|
||||
if (cmd.isNotEmpty()) onKey(cmd)
|
||||
newFixedLengthResponse(Response.Status.OK, "text/plain", "ok")
|
||||
}
|
||||
else -> newFixedLengthResponse(
|
||||
Response.Status.NOT_FOUND, "text/plain", "not found"
|
||||
)
|
||||
}
|
||||
response.addHeader("Access-Control-Allow-Origin", "*")
|
||||
return response
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package de.ladkau.schwertundmagieonpebblecompanionapp
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import com.getpebble.android.kit.PebbleKit
|
||||
import com.getpebble.android.kit.util.PebbleDictionary
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.UUID
|
||||
|
||||
class PebbleStreamer(private val context: Context) {
|
||||
// Matches the unique watch app configuration ID
|
||||
private val PEBBLE_APP_UUID = UUID.fromString("6d616769-652d-c64p-ebbl-e57591684000")
|
||||
private val KEY_FRAMEBUFFER = 0
|
||||
|
||||
private val c64Width = 320
|
||||
private val c64Height = 200
|
||||
private val targetWidth = 144
|
||||
private val targetHeight = 168
|
||||
|
||||
private val rawVideoByteBuffer = ByteBuffer.allocateDirect(c64Width * c64Height * 4)
|
||||
private val previousPackedFrame = ByteArray(3024) // Base baseline to identify delta frames
|
||||
|
||||
fun processAndStreamFrame(engine: C64Engine) {
|
||||
rawVideoByteBuffer.clear()
|
||||
engine.getRawVideoBuffer(rawVideoByteBuffer)
|
||||
rawVideoByteBuffer.rewind()
|
||||
|
||||
// 1. Downsample raw output directly to 144x168 target matrix via threshold loop
|
||||
val packedFrame = ByteArray(3024) // 144 * 168 / 8 bits
|
||||
var byteIndex = 0
|
||||
var bitCounter = 0
|
||||
var currentByte = 0
|
||||
|
||||
// Calculate steps for nearest-neighbor scaling while stripping non-essential margins
|
||||
val scaleX = c64Width.toFloat() / targetWidth.toFloat()
|
||||
val scaleY = c64Height.toFloat() / targetHeight.toFloat()
|
||||
|
||||
for (y in 0 until targetHeight) {
|
||||
val sourceY = (y * scaleY).toInt().coerceIn(0, c64Height - 1)
|
||||
for (x in 0 until targetWidth) {
|
||||
val sourceX = (x * scaleX).toInt().coerceIn(0, c64Width - 1)
|
||||
|
||||
// Read Pixel Integer (ARGB)
|
||||
val pixelIdx = (sourceY * c64Width + sourceX) * 4
|
||||
val r = rawVideoByteBuffer.get(pixelIdx + 1).toInt() and 0xFF
|
||||
val g = rawVideoByteBuffer.get(pixelIdx + 2).toInt() and 0xFF
|
||||
val b = rawVideoByteBuffer.get(pixelIdx + 3).toInt() and 0xFF
|
||||
|
||||
// Traditional relative luminance formula for 1-bit thresholding
|
||||
val luminance = (0.299 * r + 0.587 * g + 0.114 * b)
|
||||
val bit = if (luminance > 128) 1 else 0 // 1 = White background, 0 = Dark Text
|
||||
|
||||
// Pack bits securely into destination byte indices
|
||||
currentByte = (currentByte shl 1) or bit
|
||||
bitCounter++
|
||||
|
||||
if (bitCounter == 8) {
|
||||
packedFrame[byteIndex++] = currentByte.toByte()
|
||||
currentByte = 0
|
||||
bitCounter = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Perform simple Dirty Frame Check to preserve BLE airwaves
|
||||
if (packedFrame.contentEquals(previousPackedFrame)) {
|
||||
return // Game screen is currently static; abort transfer
|
||||
}
|
||||
System.arraycopy(packedFrame, 0, previousPackedFrame, 0, packedFrame.size)
|
||||
|
||||
// 3. Dispatch the payload out to the connected Pebble Watch
|
||||
val dict = PebbleDictionary()
|
||||
dict.addBytes(KEY_FRAMEBUFFER, packedFrame)
|
||||
PebbleKit.sendDataToPebble(context, PEBBLE_APP_UUID, dict)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,50 @@
|
||||
<?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:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_status"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Watch: Not connected"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_time"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="Sending: --:--:--"
|
||||
android:textSize="24sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="Keystrokes from watch:"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scroll_log"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_weight="1"
|
||||
android:background="#F5F5F5">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_log"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="monospace"
|
||||
android:padding="8dp"
|
||||
android:textSize="13sp" />
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1,7 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Base.Theme.SchwertUndMagieOnPebbleCompanionApp" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<!-- Customize your dark theme here. -->
|
||||
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">SchwertUndMagieOnPebbleCompanionApp</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,9 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Base.Theme.SchwertUndMagieOnPebbleCompanionApp" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<!-- Customize your light theme here. -->
|
||||
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
|
||||
</style>
|
||||
|
||||
<style name="Theme.SchwertUndMagieOnPebbleCompanionApp" parent="Base.Theme.SchwertUndMagieOnPebbleCompanionApp" />
|
||||
</resources>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.ladkau.schwertundmagieonpebblecompanionapp
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||