mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-09-19 04:59:59 +00:00
wear: M0 - scaffold :wear module with bitchat theme, verified on Pixel Watch 3
This commit is contained in:
parent
11d0bd794e
commit
a355ed79d0
241
docs/wear-os-implementation-plan.md
Normal file
241
docs/wear-os-implementation-plan.md
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
# Bitchat for Pixel Watch — Implementation Plan
|
||||||
|
|
||||||
|
> **Status tracker**: each milestone carries a status (`pending` / `in-progress` / `done`) and a
|
||||||
|
> checklist. A milestone may only be started when the previous milestone's success criteria pass.
|
||||||
|
> Update statuses in this file as work progresses.
|
||||||
|
|
||||||
|
| Milestone | Title | Status |
|
||||||
|
|-----------|-------|--------|
|
||||||
|
| M0 | Scaffolding & plan document | done |
|
||||||
|
| M1 | Shared core compiles on Wear | pending |
|
||||||
|
| M2 | BLE transport & background service on watch | pending |
|
||||||
|
| M3 | Global chat | pending |
|
||||||
|
| M4 | Noise DMs & people screen | pending |
|
||||||
|
| M5 | File/image receive & display — **DEFERRED** (post-M7, later day) | deferred |
|
||||||
|
| M6 | ADB test hook & mesh_lab interop | pending |
|
||||||
|
| M7 | Polish & final design pass | pending |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Context for a fresh coding agent
|
||||||
|
|
||||||
|
- **Reference app**: this repository (`bitchat-android`) is a fully working, decentralized BLE mesh
|
||||||
|
chat client. Single Gradle module `:app`, root package `com.bitchat.android`, applicationId
|
||||||
|
`com.bitchat.droid`. See `AGENTS.md` for the full architecture overview.
|
||||||
|
- **Goal**: a new `:wear` Gradle module (applicationId `com.bitchat.watch`) — a standalone Wear OS
|
||||||
|
app for the Pixel Watch. **Bluetooth mesh only**: global chat, Noise-encrypted direct messages,
|
||||||
|
and receiving/displaying files & images. It must be a fully interoperable bitchat client: scan,
|
||||||
|
advertise, connect, relay, handshake, and exchange messages with the Android (and iOS) apps.
|
||||||
|
- **Explicitly out of scope**: no internet features (no Nostr, no Tor/Arti, no relays), no GPS /
|
||||||
|
geohash / location channels, no Wi-Fi Aware, no hotspot/APK sharing, no voice notes recording.
|
||||||
|
The watch manifest must not even declare `INTERNET` or location permissions.
|
||||||
|
- **Hard constraint**: **zero modifications to `:app` production code.** The only allowed changes
|
||||||
|
to shared repo files are: `settings.gradle.kts` (add `include(":wear")`), entries in
|
||||||
|
`gradle/libs.versions.toml` (new wear dependencies only), the new `wear/` directory, and docs.
|
||||||
|
All shared Kotlin code is consumed by the `:wear` module *in place* via Gradle source sets —
|
||||||
|
files are never moved, copied, or edited.
|
||||||
|
|
||||||
|
## 2. Code reuse strategy (shared source sets)
|
||||||
|
|
||||||
|
Wear OS is Android. `android.util.Log`, `android.bluetooth.*`, `EncryptedSharedPreferences`
|
||||||
|
(androidx.security), BouncyCastle, and coroutines all work on the watch, so the vast majority of
|
||||||
|
the bitchat protocol stack compiles unmodified.
|
||||||
|
|
||||||
|
In `wear/build.gradle.kts`:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
sourceSets["main"].java.srcDir("../app/src/main/java") // with include filters (see below)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Include** (iteratively refined by fixing compile errors — the include list lives in
|
||||||
|
`wear/build.gradle.kts` with comments):
|
||||||
|
|
||||||
|
- `protocol/**` — wire format, `BinaryProtocol`, `CompressionUtil`, `MessagePadding`
|
||||||
|
- `model/**` — `BitchatMessage`, `BitchatFilePacket`, `FragmentPayload`, `NoiseEncrypted`,
|
||||||
|
`IdentityAnnouncement`, `RoutedPacket`
|
||||||
|
- `noise/**` — `NoiseSession`, `NoiseSessionManager`, `NoiseEncryptionService`,
|
||||||
|
`NoiseChannelEncryption`, vendored pure-Java `noise/southernstorm/**`
|
||||||
|
- `crypto/**` — `EncryptionService`
|
||||||
|
- `identity/**` — `SecureIdentityStateManager`
|
||||||
|
- `mesh/**` — BLE stack (`BluetoothConnectionManager`, GATT server/client managers, broadcaster,
|
||||||
|
tracker, permission manager), `FragmentManager`, `SecurityManager`, `PacketProcessor`,
|
||||||
|
`MessageHandler`, `PeerManager`, `StoreForwardManager`, `MeshTransport`, `MeshService`,
|
||||||
|
`TransferProgressManager`, `PrivateMediaTransfer`, `PowerManager`
|
||||||
|
- `services/AppStateStore.kt` — process-wide state store
|
||||||
|
- `util/AppConstants.kt` — shared constants (GATT UUIDs, fragmentation sizes)
|
||||||
|
- Small transitive deps the compiler reveals (known: `ui/debug/DebugSettingsManager.kt` is
|
||||||
|
referenced from the mesh layer — include the file, not the package)
|
||||||
|
|
||||||
|
**Exclude**: `ui/**` (except forced single-file includes), `onboarding/**`, `nostr/**`, `net/**`,
|
||||||
|
`geohash/**`, `wifi-aware/**`, `hotspot/**`, `features/voice/**`, `service/MeshForegroundService.kt`
|
||||||
|
(wear gets its own service), `BitchatApplication.kt`, `MainActivity.kt`.
|
||||||
|
|
||||||
|
**Tests**: the app's own unit tests for shared packages (`protocol`, `noise`, `crypto`, `mesh`)
|
||||||
|
are wired into the `:wear` test source set the same way (srcDir + includes), so shared behavior is
|
||||||
|
continuously verified on both modules.
|
||||||
|
|
||||||
|
**Resources**: font files cannot be selectively shared via srcDir cleanly — copy the 4 Geist Mono
|
||||||
|
font files (`app/src/main/res/font/geist_mono_*`) into `wear/src/main/res/font/`. Theme/palette/
|
||||||
|
peer-color logic is re-created as wear-owned files mirroring `ui/theme/` values exactly.
|
||||||
|
|
||||||
|
## 3. Watch UX design
|
||||||
|
|
||||||
|
Wear Compose Material3 (round-screen safe by default):
|
||||||
|
|
||||||
|
- **Screens**: Chat (global timeline) → People (connected peers w/ RSSI, unread badges) →
|
||||||
|
DM conversation. Edge-swipe back, `TimeText` scaffold, rotary crown scrolling.
|
||||||
|
- **Visual identity** (mirrors `ui/theme/` exactly): black background `#000000`, green primary
|
||||||
|
`#32D74B`, error `#FF453A`, orange accent for self/mentions, djb2-hash stable peer colors
|
||||||
|
(`PeerColors.kt` algorithm), Geist Mono typography, `BitchatMotion` timing tokens
|
||||||
|
(120/180/240 ms) for all animations.
|
||||||
|
- **Input**: text field using the Pixel Watch Gboard IME, plus voice dictation via
|
||||||
|
`RecognizerIntent`. Haptic feedback on incoming messages.
|
||||||
|
- **Background**: wear-owned foreground service (type `connectedDevice`) keeps scan/advertise
|
||||||
|
alive; shared `PowerManager` provides duty-cycling.
|
||||||
|
|
||||||
|
## 4. Hardware & test environment
|
||||||
|
|
||||||
|
- Pixel Watch connected via ADB (target device for all milestones; screencaps via
|
||||||
|
`adb exec-out screencap`).
|
||||||
|
- Two phones running the Android bitchat app, also on ADB, for interop testing (used heavily from
|
||||||
|
M6; manual interop checks from M2 onward).
|
||||||
|
- Design verification: at every UI milestone, take ADB screencaps of every screen and review them
|
||||||
|
for round-screen clipping, element visibility, contrast, and touch-target size. A milestone does
|
||||||
|
not pass until its screencap set is approved.
|
||||||
|
|
||||||
|
## 5. Risks & notes
|
||||||
|
|
||||||
|
- `BluetoothMeshService` (legacy monolith) vs `MeshCore` — prefer wiring the shared components
|
||||||
|
directly (MeshCore-style composition) in the wear service.
|
||||||
|
- `mesh/` references `ui/debug/DebugSettingsManager` — include that single file; do not pull in
|
||||||
|
the debug UI sheet.
|
||||||
|
- Wear BLE MTUs are small; the shared fragmentation layer (469-byte fragments) already handles
|
||||||
|
this.
|
||||||
|
- `EncryptedSharedPreferences` (androidx.security-crypto) works on Wear OS — identity persistence
|
||||||
|
is reused as-is.
|
||||||
|
- Watch has no camera/gallery: file transfer is **receive + display only** (confirmed decision).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Milestones
|
||||||
|
|
||||||
|
### M0 — Scaffolding & plan document
|
||||||
|
|
||||||
|
- [x] Write this plan to `docs/wear-os-implementation-plan.md`
|
||||||
|
- [x] Create `:wear` module: `wear/build.gradle.kts`, manifest
|
||||||
|
(`<uses-feature android:name="android.hardware.type.watch"/>`, standalone, BT permissions,
|
||||||
|
**no INTERNET/location**), `MainActivity` with hello-world screen using the ported theme
|
||||||
|
- [x] Add Wear Compose dependencies to `gradle/libs.versions.toml`; `include(":wear")` in
|
||||||
|
`settings.gradle.kts`
|
||||||
|
- [x] Build, install, and launch on the physical Pixel Watch via ADB; take first screencap
|
||||||
|
|
||||||
|
**Success criteria**: `./gradlew :wear:assembleDebug` green; app launches on the watch;
|
||||||
|
`git diff --name-only` shows no changes under `app/src/`.
|
||||||
|
**Result**: PASSED — installed on Pixel Watch 3 (serial 4C201JEAYW0020), launch screencap shows
|
||||||
|
"bitchat" wordmark (green `#32D74B`, Geist Mono, black background) correctly centered on the
|
||||||
|
round display. No `app/src/` changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### M1 — Shared core compiles on Wear
|
||||||
|
|
||||||
|
- [ ] Configure srcDir include list in `wear/build.gradle.kts`; resolve transitive dependencies by
|
||||||
|
extending includes (never by copying Kotlin sources)
|
||||||
|
- [ ] Copy Geist Mono fonts; create wear theme/palette/peer-color files mirroring `ui/theme/`
|
||||||
|
- [ ] Wire shared unit tests (`protocol`, `noise`, `crypto`, `mesh`) into `:wear` test source set
|
||||||
|
- [ ] `./gradlew :app:test :wear:test` green
|
||||||
|
|
||||||
|
**Success criteria**: the entire shared stack (protocol, noise, crypto, identity, mesh, model,
|
||||||
|
AppStateStore) compiles into `:wear`; both modules' unit tests pass; `app/src/` untouched.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### M2 — BLE transport & background service on watch
|
||||||
|
|
||||||
|
- [ ] Wear onboarding flow: Bluetooth-enable check + runtime permission requests
|
||||||
|
(`BLUETOOTH_SCAN/CONNECT/ADVERTISE`), watch-styled screens
|
||||||
|
- [ ] `WearMeshService` foreground service (type `connectedDevice`); wire shared
|
||||||
|
`BluetoothConnectionManager` + mesh components; start scanning + advertising
|
||||||
|
- [ ] Internal debug screen: discovered peers with RSSI (temporary, replaced by real UI in M3/M4)
|
||||||
|
- [ ] Manual interop check: watch and one phone mutually discover
|
||||||
|
|
||||||
|
**Success criteria**: the phone's bitchat app lists the watch as a connected peer and vice versa
|
||||||
|
(logcat + screencap evidence); mesh survives the screen turning off (ambient mode) for 5 minutes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### M3 — Global chat
|
||||||
|
|
||||||
|
- [ ] Nickname onboarding; identity announcement over the mesh
|
||||||
|
- [ ] Send/receive/relay public `BitchatMessage`s (relay/TTL comes free from shared mesh code)
|
||||||
|
- [ ] Chat timeline UI (`ScalingLazyColumn`, message bubbles per bitchat style, peer colors,
|
||||||
|
timestamps) + composer (IME + `RecognizerIntent` dictation) + incoming-message haptics
|
||||||
|
- [ ] Design check: ADB screencaps of onboarding, chat (empty/populated), composer; review for
|
||||||
|
round-screen clipping/visibility/contrast
|
||||||
|
|
||||||
|
**Success criteria**: two-way public chat between watch and phone; messages the watch relays reach
|
||||||
|
a second phone that is only connected through the first (relay proof); screencap set approved.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### M4 — Noise DMs & people screen
|
||||||
|
|
||||||
|
- [ ] People screen: connected peers, nicknames, RSSI, unread-DM badges
|
||||||
|
- [ ] Tap peer → Noise XX handshake (shared `EncryptionService`/`NoiseSessionManager`) → DM thread
|
||||||
|
- [ ] DM conversation UI; unread counters; delivery/read receipts if supported by shared code
|
||||||
|
- [ ] Identity persistence (`EncryptedSharedPreferences`); stale-session detection & automatic
|
||||||
|
re-handshake after watch app restart
|
||||||
|
- [ ] Design check: screencaps of people screen, handshake state, DM thread
|
||||||
|
|
||||||
|
**Success criteria**: encrypted DM round trip with the phone; DMs survive a watch app restart
|
||||||
|
(session recovery); screencap set approved.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### M5 — File/image receive & display — **DEFERRED**
|
||||||
|
|
||||||
|
> Deferred to a later day (after M7). Milestones M6 and M7 do not depend on M5 and proceed
|
||||||
|
> without it. The mesh_lab `file` scenario for the watch is skipped until M5 is un-deferred.
|
||||||
|
|
||||||
|
- [ ] Receive broadcast files (`MessageType.FILE_TRANSFER`, `BitchatFilePacket` TLV decode,
|
||||||
|
fragment reassembly — all shared)
|
||||||
|
- [ ] Receive Noise-encrypted private files (`NoisePayloadType.FILE_TRANSFER`)
|
||||||
|
- [ ] Inline image rendering in chat timelines; full-screen image viewer (pinch/crown zoom);
|
||||||
|
non-image files saved with a way to open/share them
|
||||||
|
- [ ] Transfer progress indicator; respect shared fragment/size caps
|
||||||
|
- [ ] Design check: screencaps of inline image, full-screen viewer, transfer progress
|
||||||
|
|
||||||
|
**Success criteria**: phone→watch image renders inline in both global chat and DM; SHA-256 of
|
||||||
|
received file matches sender; screencap set approved. (Sending files from the watch is out of
|
||||||
|
scope.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### M6 — ADB test hook & mesh_lab interop
|
||||||
|
|
||||||
|
- [ ] Wear debug-only `TestHookReceiver` (`wear/src/debug/`) mirroring the phone's command set:
|
||||||
|
`ping`, `start`, `stop`, `whoami`, `set_nickname`, `scan`, `peers`, `connect`, `handshake`,
|
||||||
|
`session`, `announce`, `broadcast_msg`, `dm_send`, `dm_recv`, `msg_recv`, `file_recv`, `state`,
|
||||||
|
`clear_results` — broadcast action `com.bitchat.watch.TEST_HOOK`, same JSON-result-file protocol
|
||||||
|
- [ ] Extend `tools/release_gate/mesh_lab.py`: `--serial-watch` argument and phone↔watch scenarios
|
||||||
|
(`dm`, `broadcast`, `session_recovery`, `all`; `file` excluded while M5 is deferred), reusing
|
||||||
|
the existing `Device`/`cmd` machinery
|
||||||
|
- [ ] Run full scenario suite phone↔watch; store evidence JSON
|
||||||
|
|
||||||
|
**Success criteria**:
|
||||||
|
`python3 tools/release_gate/mesh_lab.py scenario all --serial-a <phone> --serial-watch <watch>`
|
||||||
|
exits 0 with evidence files; no manual intervention.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### M7 — Polish & final design pass
|
||||||
|
|
||||||
|
- [ ] Animations/transitions per `BitchatMotion` tokens; message-appear animations; screen
|
||||||
|
transitions; rotary scroll feel; splash screen & app icon (bitchat wordmark style)
|
||||||
|
- [ ] Power/battery: verify shared `PowerManager` duty-cycling behaves on Wear; ambient-mode
|
||||||
|
behavior; memory audit (fragment/image caps)
|
||||||
|
- [ ] Full screencap design review of every screen and state; fix all findings
|
||||||
|
- [ ] Update this document: all milestones `done`; add a short "how to build/run/test" section
|
||||||
|
|
||||||
|
**Success criteria**: all milestones marked `done`; interop suite green; final screencap set
|
||||||
|
approved; a fresh agent can build, install, and test the watch app from this document alone.
|
||||||
@ -21,6 +21,10 @@ navigation-compose = "2.9.8"
|
|||||||
# Accompanist
|
# Accompanist
|
||||||
accompanist-permissions = "0.37.3"
|
accompanist-permissions = "0.37.3"
|
||||||
|
|
||||||
|
# Wear OS
|
||||||
|
wear-compose = "1.6.2"
|
||||||
|
wear-tooling-preview = "1.0.0"
|
||||||
|
|
||||||
# Cryptography
|
# Cryptography
|
||||||
bouncycastle = "1.85"
|
bouncycastle = "1.85"
|
||||||
tink-android = "1.23.0"
|
tink-android = "1.23.0"
|
||||||
@ -95,6 +99,11 @@ androidx-navigation-compose = { module = "androidx.navigation:navigation-compose
|
|||||||
# Accompanist
|
# Accompanist
|
||||||
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist-permissions" }
|
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist-permissions" }
|
||||||
|
|
||||||
|
# Wear OS
|
||||||
|
androidx-wear-compose-foundation = { module = "androidx.wear.compose:compose-foundation", version.ref = "wear-compose" }
|
||||||
|
androidx-wear-compose-material3 = { module = "androidx.wear.compose:compose-material3", version.ref = "wear-compose" }
|
||||||
|
androidx-wear-tooling-preview = { module = "androidx.wear:wear-tooling-preview", version.ref = "wear-tooling-preview" }
|
||||||
|
|
||||||
# Cryptography
|
# Cryptography
|
||||||
bouncycastle-bcprov = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncycastle" }
|
bouncycastle-bcprov = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncycastle" }
|
||||||
google-tink-android = { module = "com.google.crypto.tink:tink-android", version.ref = "tink-android" }
|
google-tink-android = { module = "com.google.crypto.tink:tink-android", version.ref = "tink-android" }
|
||||||
|
|||||||
@ -17,4 +17,5 @@ dependencyResolutionManagement {
|
|||||||
|
|
||||||
rootProject.name = "bitchat-android"
|
rootProject.name = "bitchat-android"
|
||||||
include(":app")
|
include(":app")
|
||||||
|
include(":wear")
|
||||||
// Using published Arti AAR; local module not included
|
// Using published Arti AAR; local module not included
|
||||||
|
|||||||
89
wear/build.gradle.kts
Normal file
89
wear/build.gradle.kts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application)
|
||||||
|
alias(libs.plugins.kotlin.parcelize)
|
||||||
|
alias(libs.plugins.kotlin.compose)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.bitchat.watch"
|
||||||
|
compileSdk = libs.versions.compileSdk.get().toInt()
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "com.bitchat.watch"
|
||||||
|
minSdk = 30 // Wear OS 3 (Pixel Watch 1); BLE APIs match the phone app's usage
|
||||||
|
targetSdk = libs.versions.targetSdk.get().toInt()
|
||||||
|
versionCode = 1
|
||||||
|
versionName = "0.1.0"
|
||||||
|
|
||||||
|
vectorDrawables {
|
||||||
|
useSupportLibrary = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = true
|
||||||
|
isShrinkResources = true
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
compose = true
|
||||||
|
buildConfig = true
|
||||||
|
}
|
||||||
|
packaging {
|
||||||
|
resources {
|
||||||
|
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lint {
|
||||||
|
abortOnError = false
|
||||||
|
checkReleaseBuilds = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget.set(JvmTarget.JVM_11)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.androidx.core.ktx)
|
||||||
|
implementation(libs.androidx.activity.compose)
|
||||||
|
|
||||||
|
// Wear Compose
|
||||||
|
implementation(libs.androidx.wear.compose.foundation)
|
||||||
|
implementation(libs.androidx.wear.compose.material3)
|
||||||
|
implementation(libs.androidx.wear.tooling.preview)
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
implementation(libs.bundles.lifecycle)
|
||||||
|
implementation(libs.androidx.lifecycle.process)
|
||||||
|
|
||||||
|
// Coroutines
|
||||||
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
|
|
||||||
|
// Cryptography (shared Noise/encryption stack)
|
||||||
|
implementation(libs.bouncycastle.bcprov)
|
||||||
|
|
||||||
|
// JSON (BitchatMessage model)
|
||||||
|
implementation(libs.gson)
|
||||||
|
|
||||||
|
// Security preferences (Noise identity persistence)
|
||||||
|
implementation(libs.androidx.security.crypto)
|
||||||
|
|
||||||
|
// Testing
|
||||||
|
testImplementation(libs.bundles.testing)
|
||||||
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||||
|
}
|
||||||
43
wear/src/main/AndroidManifest.xml
Normal file
43
wear/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?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-feature android:name="android.hardware.type.watch" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.bluetooth_le"
|
||||||
|
android:required="true" />
|
||||||
|
|
||||||
|
<!-- Bluetooth mesh only. Deliberately NO android.permission.INTERNET and no location. -->
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.BLUETOOTH_SCAN"
|
||||||
|
android:usesPermissionFlags="neverForLocation"
|
||||||
|
tools:targetApi="s" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="false"
|
||||||
|
android:icon="@drawable/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@android:style/Theme.DeviceDefault">
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.wearable.standalone"
|
||||||
|
android:value="true" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:taskAffinity="">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
52
wear/src/main/java/com/bitchat/watch/MainActivity.kt
Normal file
52
wear/src/main/java/com/bitchat/watch/MainActivity.kt
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package com.bitchat.watch
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
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.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.wear.compose.material3.MaterialTheme
|
||||||
|
import androidx.wear.compose.material3.Text
|
||||||
|
import com.bitchat.watch.ui.theme.BitchatWearTheme
|
||||||
|
|
||||||
|
class MainActivity : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContent {
|
||||||
|
BitchatWearTheme {
|
||||||
|
PlaceholderScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PlaceholderScreen() {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "bitchat",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "mesh initializing",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(top = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
package com.bitchat.watch.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class BitchatPalette(
|
||||||
|
val inputOutline: Color,
|
||||||
|
val inputOutlineFocused: Color,
|
||||||
|
val inputSurface: Color,
|
||||||
|
val inputSurfaceFocused: Color,
|
||||||
|
val inputButton: Color,
|
||||||
|
val textTertiary: Color,
|
||||||
|
val accentOrange: Color,
|
||||||
|
val accentPurple: Color,
|
||||||
|
val peerColors: PeerColorStyle,
|
||||||
|
)
|
||||||
|
|
||||||
|
val DarkBitchatPalette = BitchatPalette(
|
||||||
|
inputOutline = Color(0xFF333635),
|
||||||
|
inputOutlineFocused = Color(0xFF5A605D),
|
||||||
|
inputSurface = Color(0xFF0B0B0B),
|
||||||
|
inputSurfaceFocused = Color(0xFF151515),
|
||||||
|
inputButton = Color(0xFF1E1E1E),
|
||||||
|
textTertiary = Color(0xFF6B776B),
|
||||||
|
accentOrange = Color(0xFFFF9F0A),
|
||||||
|
accentPurple = Color(0xFFBF5AF2),
|
||||||
|
peerColors = PeerColorStyle.Dark,
|
||||||
|
)
|
||||||
|
|
||||||
|
val LocalBitchatPalette = staticCompositionLocalOf { DarkBitchatPalette }
|
||||||
|
|
||||||
|
object BitchatMotion {
|
||||||
|
const val QUICK_MS = 120
|
||||||
|
const val STANDARD_MS = 180
|
||||||
|
const val EMPHASIZED_MS = 240
|
||||||
|
}
|
||||||
35
wear/src/main/java/com/bitchat/watch/ui/theme/PeerColors.kt
Normal file
35
wear/src/main/java/com/bitchat/watch/ui/theme/PeerColors.kt
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package com.bitchat.watch.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class PeerColorStyle(
|
||||||
|
val saturation: Float,
|
||||||
|
val value: Float,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val Dark = PeerColorStyle(saturation = 0.55f, value = 0.82f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun colorForPeer(stableKey: String, palette: BitchatPalette): Color {
|
||||||
|
var hash = 5381UL
|
||||||
|
for (byte in stableKey.toByteArray()) {
|
||||||
|
hash = ((hash shl 5) + hash) + byte.toUByte().toULong()
|
||||||
|
}
|
||||||
|
|
||||||
|
var hue = (hash % 360UL).toDouble() / 360.0
|
||||||
|
val orange = 30.0 / 360.0
|
||||||
|
if (abs(hue - orange) < 0.05) {
|
||||||
|
hue = (hue + 0.12) % 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
val style = palette.peerColors
|
||||||
|
return Color.hsv(
|
||||||
|
hue = (hue * 360).toFloat(),
|
||||||
|
saturation = style.saturation,
|
||||||
|
value = style.value
|
||||||
|
)
|
||||||
|
}
|
||||||
42
wear/src/main/java/com/bitchat/watch/ui/theme/Theme.kt
Normal file
42
wear/src/main/java/com/bitchat/watch/ui/theme/Theme.kt
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package com.bitchat.watch.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.wear.compose.material3.ColorScheme
|
||||||
|
import androidx.wear.compose.material3.MaterialTheme
|
||||||
|
|
||||||
|
val BitchatWearColorScheme = ColorScheme(
|
||||||
|
primary = Color(0xFF32D74B),
|
||||||
|
onPrimary = Color.Black,
|
||||||
|
primaryContainer = Color(0xFF163D1D),
|
||||||
|
onPrimaryContainer = Color(0xFFB8F5C1),
|
||||||
|
secondary = Color(0xFF0A84FF),
|
||||||
|
onSecondary = Color.Black,
|
||||||
|
secondaryContainer = Color(0xFF082E54),
|
||||||
|
onSecondaryContainer = Color(0xFFC2E0FF),
|
||||||
|
tertiary = Color(0xFFFF9F0A),
|
||||||
|
onTertiary = Color.Black,
|
||||||
|
background = Color(0xFF000000),
|
||||||
|
onBackground = Color(0xFFF5F5F5),
|
||||||
|
surfaceContainer = Color(0xFF0E150E),
|
||||||
|
surfaceContainerLow = Color(0xFF0B0B0B),
|
||||||
|
surfaceContainerHigh = Color(0xFF182118),
|
||||||
|
onSurface = Color(0xFFF5F5F5),
|
||||||
|
onSurfaceVariant = Color(0xFF9AA69A),
|
||||||
|
outline = Color(0xFF2A3A2A),
|
||||||
|
outlineVariant = Color(0xFF1C271C),
|
||||||
|
error = Color(0xFFFF453A),
|
||||||
|
onError = Color.Black,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BitchatWearTheme(content: @Composable () -> Unit) {
|
||||||
|
CompositionLocalProvider(LocalBitchatPalette provides DarkBitchatPalette) {
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = BitchatWearColorScheme,
|
||||||
|
typography = BitchatWearTypography,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
43
wear/src/main/java/com/bitchat/watch/ui/theme/Typography.kt
Normal file
43
wear/src/main/java/com/bitchat/watch/ui/theme/Typography.kt
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package com.bitchat.watch.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.Font
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.wear.compose.material3.Typography
|
||||||
|
import com.bitchat.watch.R
|
||||||
|
|
||||||
|
val BitchatFontFamily = FontFamily(
|
||||||
|
Font(R.font.geist_mono_regular, FontWeight.Normal),
|
||||||
|
Font(R.font.geist_mono_medium, FontWeight.Medium),
|
||||||
|
Font(R.font.geist_mono_semibold, FontWeight.SemiBold),
|
||||||
|
Font(R.font.geist_mono_bold, FontWeight.Bold),
|
||||||
|
)
|
||||||
|
|
||||||
|
val BitchatWearTypography = Typography(
|
||||||
|
defaultFontFamily = BitchatFontFamily,
|
||||||
|
)
|
||||||
|
|
||||||
|
object ChatVisualTokens {
|
||||||
|
val MessageBodyStyle = TextStyle(
|
||||||
|
fontFamily = BitchatFontFamily,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
lineHeight = 17.sp,
|
||||||
|
)
|
||||||
|
|
||||||
|
val SenderStyle = TextStyle(
|
||||||
|
fontFamily = BitchatFontFamily,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
lineHeight = 15.sp,
|
||||||
|
)
|
||||||
|
|
||||||
|
val SystemActionStyle = TextStyle(
|
||||||
|
fontFamily = BitchatFontFamily,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
lineHeight = 14.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
16
wear/src/main/res/drawable/ic_launcher.xml
Normal file
16
wear/src/main/res/drawable/ic_launcher.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="48dp"
|
||||||
|
android:height="48dp"
|
||||||
|
android:viewportWidth="48"
|
||||||
|
android:viewportHeight="48">
|
||||||
|
<path
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:pathData="M0,0h48v48h-48z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#32D74B"
|
||||||
|
android:pathData="M10,14l10,10l-10,10v-6l5,-4l-5,-4z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#32D74B"
|
||||||
|
android:pathData="M22,32h16v4h-16z" />
|
||||||
|
</vector>
|
||||||
BIN
wear/src/main/res/font/geist_mono_bold.ttf
Normal file
BIN
wear/src/main/res/font/geist_mono_bold.ttf
Normal file
Binary file not shown.
BIN
wear/src/main/res/font/geist_mono_medium.ttf
Normal file
BIN
wear/src/main/res/font/geist_mono_medium.ttf
Normal file
Binary file not shown.
BIN
wear/src/main/res/font/geist_mono_regular.ttf
Normal file
BIN
wear/src/main/res/font/geist_mono_regular.ttf
Normal file
Binary file not shown.
BIN
wear/src/main/res/font/geist_mono_semibold.ttf
Normal file
BIN
wear/src/main/res/font/geist_mono_semibold.ttf
Normal file
Binary file not shown.
4
wear/src/main/res/values/strings.xml
Normal file
4
wear/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">bitchat</string>
|
||||||
|
</resources>
|
||||||
Loading…
x
Reference in New Issue
Block a user