feat: added companion app for WearOS, and setted up icons

This commit is contained in:
2026-06-10 11:06:35 +03:00
parent 70ba200d7a
commit c21708331c
57 changed files with 566 additions and 116 deletions
+7
View File
@@ -1,6 +1,7 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
kotlin("plugin.serialization") version "2.2.20"
}
android {
@@ -42,11 +43,14 @@ android {
dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.compose.foundation)
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.constraintlayout)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.wear.tooling.preview)
implementation(libs.compose.ui.tooling)
@@ -57,4 +61,7 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling)
implementation(libs.zxing.core)
implementation(libs.zxing.android)
implementation(libs.material)
implementation("com.google.android.gms:play-services-wearable:18.1.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
}
+11 -1
View File
@@ -11,13 +11,23 @@
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@android:style/Theme.DeviceDefault">
<service android:name=".presentation.MessagesReceiver" android:exported="true">
<intent-filter>
<action android:name="com.google.android.gms.wearable.BIND_LISTENER" />
</intent-filter>
</service>
<activity
android:name=".presentation.ShowCardActivity"
android:exported="false"
android:theme="@android:style/Theme.DeviceDefault.NoActionBar" />
<uses-library
android:name="com.google.android.wearable"
android:required="true" />
<uses-library
android:name="wear-sdk"
android:required="false" />
<!--
Set to true if your app is Standalone, that is, it does not require the handheld
app to run.
Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

@@ -0,0 +1,12 @@
package ru.omni_devel.cards.presentation
import com.google.zxing.BarcodeFormat
import kotlinx.serialization.Serializable
@Serializable
class CardInfo (
val id: Int,
val name: String,
val codeType: BarcodeFormat,
val codeValue: String,
)
@@ -0,0 +1,92 @@
package ru.omni_devel.cards.presentation
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import com.google.zxing.BarcodeFormat
class DbHelper(val context: Context, val factory: SQLiteDatabase.CursorFactory?) : SQLiteOpenHelper(context, "omni_cards", factory, 1) {
override fun onCreate(db: SQLiteDatabase?) {
db!!.execSQL("CREATE TABLE IF NOT EXISTS cards (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, codeType TEXT, codeValue TEXT)")
}
override fun onUpgrade(
db: SQLiteDatabase?,
p1: Int,
p2: Int
) {
db!!.execSQL("DROP TABLE IF EXISTS cards")
}
fun clearDatabase() {
val db = this.writableDatabase
db.execSQL("DELETE FROM cards")
db.close()
}
fun addCards(cards: List<CardInfo>) {
val db = this.writableDatabase
for (card in cards) {
val values = ContentValues()
values.put("id", card.id)
values.put("name", card.name)
values.put("codeValue", card.codeValue)
values.put("codeType", card.codeType.name)
db.insert("cards", null, values)
}
db.close()
}
fun getCards(): List<CardInfo> {
val db = this.readableDatabase
val cursor = db.rawQuery("SELECT * FROM cards", null)
val cards = mutableListOf<CardInfo>()
while (cursor.moveToNext()) {
cards.add(CardInfo(
id = cursor.getInt(cursor.getColumnIndexOrThrow("id")),
name = cursor.getString(cursor.getColumnIndexOrThrow("name")),
codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))),
codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue"))
))
}
cursor.close()
db.close()
return cards
}
fun getCard(id: Int): CardInfo? {
val db = this.readableDatabase
val cursor = db.rawQuery("SELECT * FROM cards WHERE id = ?", arrayOf(id.toString()))
if (!cursor.moveToFirst()) {
cursor.close()
db.close()
return null
}
val cardInfo = CardInfo(
id = cursor.getInt(cursor.getColumnIndexOrThrow("id")),
name = cursor.getString(cursor.getColumnIndexOrThrow("name")),
codeType = BarcodeFormat.valueOf(cursor.getString(cursor.getColumnIndexOrThrow("codeType"))),
codeValue = cursor.getString(cursor.getColumnIndexOrThrow("codeValue"))
)
cursor.close()
db.close()
return cardInfo
}
}
@@ -1,115 +1,100 @@
/* While this template provides a good starting point for using Wear Compose, you can always
* take a look at https://github.com/android/wear-os-samples/tree/main/ComposeStarter to find the
* most up to date changes to the libraries and their usages.
*/
package ru.omni_devel.cards.presentation
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
import androidx.wear.compose.material3.AppScaffold
import androidx.wear.compose.material3.Button
import androidx.wear.compose.material3.ButtonDefaults
import androidx.wear.compose.material3.EdgeButton
import androidx.wear.compose.material3.ListHeader
import androidx.wear.compose.material3.MaterialTheme
import androidx.wear.compose.material3.ScreenScaffold
import androidx.wear.compose.material3.SurfaceTransformation
import androidx.wear.compose.material3.Text
import androidx.wear.compose.material3.lazy.rememberTransformationSpec
import androidx.wear.compose.material3.lazy.transformedHeight
import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices
import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales
import ru.omni_devel.cards.R
import ru.omni_devel.cards.presentation.theme.OmniCardsTheme
class MainActivity : ComponentActivity() {
private lateinit var db: DbHelper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
db = DbHelper(this, null)
setContent {
WearApp("Android")
App(db.getCards())
}
}
}
@Composable
fun WearApp(greetingName: String) {
OmniCardsTheme {
AppScaffold {
val listState = rememberTransformingLazyColumnState()
val transformationSpec = rememberTransformationSpec()
ScreenScaffold(
scrollState = listState,
edgeButton = {
EdgeButton(
onClick = { /*TODO*/ },
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
),
) {
Text("More")
}
},
) { contentPadding -> // ScreenScaffold provides default padding; adjust as needed
TransformingLazyColumn(contentPadding = contentPadding, state = listState) {
item {
ListHeader(
modifier =
Modifier.fillMaxWidth().transformedHeight(this, transformationSpec),
transformation = SurfaceTransformation(transformationSpec),
) {
Text(text = stringResource(R.string.hello_world, greetingName))
}
}
item {
Button(
onClick = { /*TODO*/ },
modifier = Modifier.fillMaxWidth()
.transformedHeight(this, transformationSpec),
transformation = SurfaceTransformation(transformationSpec),
) {
Text("Button A")
}
}
item {
Button(
onClick = { /*TODO*/ },
modifier = Modifier.fillMaxWidth()
.transformedHeight(this, transformationSpec),
transformation = SurfaceTransformation(transformationSpec),
) {
Text("Button B")
}
}
item {
Button(
onClick = { /*TODO*/ },
modifier = Modifier.fillMaxWidth()
.transformedHeight(this, transformationSpec),
transformation = SurfaceTransformation(transformationSpec),
) {
Text("Button C")
}
}
fun App(cards: List<CardInfo>) {
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
val context = LocalContext.current
TransformingLazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(
top = maxHeight * 0.4f,
bottom = maxHeight * 0.4f
),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
if (cards.isEmpty()) {
item {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
val intent = Intent(context, ShowCardActivity::class.java)
intent.putExtra("cardId", 1)
context.startActivity(intent)
}
) {
Text("Debug")
}
}
item {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Sync with mobile APP to get started")
}
}
} else {
for (card in cards) {
item {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
val intent = Intent(context, ShowCardActivity::class.java)
intent.putExtra("cardId", card.id)
context.startActivity(intent)
}
) {
Text(
text = card.name,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
}
}
}
}
}
}
}
@WearPreviewDevices
@WearPreviewFontScales
@Composable
fun DefaultPreview() {
WearApp("Preview Android")
}
@@ -0,0 +1,43 @@
package ru.omni_devel.cards.presentation
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.widget.Toast
import com.google.android.gms.wearable.MessageEvent
import com.google.android.gms.wearable.WearableListenerService
import kotlinx.serialization.json.Json
class MessagesReceiver : WearableListenerService() {
private lateinit var db: DbHelper
override fun onCreate() {
super.onCreate()
db = DbHelper(this, null)
}
override fun onMessageReceived(event: MessageEvent) {
when (event.path) {
"/updateCards" -> {
val json = String(event.data, Charsets.UTF_8)
val cards = Json.decodeFromString<List<CardInfo>>(json)
Toast.makeText(this, "Updating database...", Toast.LENGTH_SHORT).show()
db.clearDatabase()
db.addCards(cards)
Handler(Looper.getMainLooper()).post {
Toast.makeText(this, "Success!", Toast.LENGTH_SHORT).show()
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
startActivity(intent)
}
}
}
}
}
@@ -0,0 +1,93 @@
package ru.omni_devel.cards.presentation
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalDensity
class ShowCardActivity : ComponentActivity() {
private lateinit var db: DbHelper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
Handler(Looper.getMainLooper()).postDelayed({
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}, 15_000L)
db = DbHelper(this, null)
val cardId = intent.getIntExtra("cardId", 0)
val cardInfo = db.getCard(cardId)
if (cardInfo == null) {
Toast.makeText(this, "cardInfo is null", Toast.LENGTH_LONG).show()
finish()
return
}
setContent {
CardView(cardInfo)
}
}
override fun onResume() {
super.onResume()
setBrightness(1.0f)
}
override fun onPause() {
super.onPause()
setBrightness(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE)
}
private fun setBrightness(level: Float) {
val params = window.attributes
params.screenBrightness = level
window.attributes = params
}
}
@Composable
fun CardView(cardInfo: CardInfo) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
BoxWithConstraints(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
val density = LocalDensity.current
val widthPx = with(density) { maxWidth.roundToPx() }
val heightPx = with(density) { maxHeight.roundToPx() }
Image(
bitmap = generateCode(cardInfo.codeValue, cardInfo.codeType, widthPx, heightPx).asImageBitmap(),
contentDescription = "Card code"
)
}
}
}
@@ -0,0 +1,13 @@
package ru.omni_devel.cards.presentation
import android.graphics.Bitmap
import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter
import com.journeyapps.barcodescanner.BarcodeEncoder
fun generateCode(value: String, format: BarcodeFormat, width: Int, height: Int): Bitmap {
val writer = MultiFormatWriter()
val matrix = writer.encode(value, format, width, height)
return BarcodeEncoder().createBitmap(matrix)
}
@@ -0,0 +1,21 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
@@ -0,0 +1,21 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
@@ -1,6 +0,0 @@
<?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>
@@ -1,6 +0,0 @@
<?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.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 20 KiB

+33
View File
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Backgrounds -->
<color name="bg_app">#0B0E17</color>
<color name="bg_surface">#0E1118</color>
<color name="bg_card">#131726</color>
<color name="bg_elevated">#1A2235</color>
<color name="bg_hover">#212B42</color>
<!-- Accent (indigo) -->
<color name="accent_subtle">#152045</color>
<color name="accent_primary">#2952C8</color>
<color name="accent_hover">#3D6AE8</color>
<color name="accent_light">#6B90FF</color>
<!-- Text -->
<color name="text_primary">#DCE3F5</color>
<color name="text_secondary">#8A9BBC</color>
<color name="text_hint">#4E5E80</color>
<!-- Borders -->
<color name="border_subtle">#1C2030</color>
<color name="border_default">#1C2A42</color>
<color name="border_active">#2A3D60</color>
<!-- Semantic -->
<color name="success_bg">#0F3320</color>
<color name="success">#2ECC71</color>
<color name="error_bg">#3D1515</color>
<color name="error">#E05555</color>
<color name="warning_bg">#3D2E00</color>
<color name="warning">#F0A500</color>
</resources>
@@ -0,0 +1,19 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#000000</color>
</resources>
+5 -3
View File
@@ -1,8 +1,10 @@
<resources>
<style name="MainActivityTheme.Starting" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@android:color/black</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
<item name="postSplashScreenTheme">@android:style/Theme.DeviceDefault</item>
<item name="android:windowBackground">@color/bg_app</item>
<item name="android:textColorPrimary">@color/text_primary</item>
<item name="android:textColorSecondary">@color/text_secondary</item>
<item name="android:textColorHint">@color/text_hint</item>
<item name="android:colorBackground">@color/bg_app</item>
</style>
</resources>