Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
bf658e4
feat: add WearOS app with GMS-based Wearable Data Layer transport
blacklight Jul 2, 2026
5a6aca2
feat: add GMS-free Bluetooth RFCOMM transport for Wear OS
blacklight Jul 2, 2026
9f6d0d4
fix: resolve two wear card view bugs
blacklight Jul 2, 2026
302af3d
fix: re-fetch cards on onResume to recover from post-reboot spinner
blacklight Jul 2, 2026
60df23c
fix: prevent BluetoothServerService from being killed by app idle
blacklight Jul 2, 2026
a1d00e9
feat: keep screen on at full brightness while barcode is displayed
blacklight Jul 2, 2026
744a745
refactor: remove all GMS/play-services-wearable dependencies
blacklight Jul 2, 2026
aa72cc1
feat: persist cards locally on watch for offline access
blacklight Jul 2, 2026
7f08786
chore(build): Set debug applicationIdSuffix
blacklight Jul 2, 2026
b7cbe83
fix: check BLUETOOTH_CONNECT permission at call site in AcceptThread
blacklight Jul 4, 2026
06c2fc5
refactor: Centralise notification/channel IDs in NotificationInfo
blacklight Jul 4, 2026
71bbb86
chore: Addressed LINT warning on rejected Bluetooth permission
blacklight Jul 4, 2026
75fd1f2
chore: Addressed LINT warnings in BluetoothServerService
blacklight Jul 4, 2026
33a36ef
feat(wear): Version the Bluetooth sync protocol
blacklight Jul 4, 2026
6b769c6
refactor(bluetooth): Move BluetoothServerService to wearos package
blacklight Jul 4, 2026
5778d3e
feat(app): Add Wear OS sync setting and gate Bluetooth server
blacklight Jul 4, 2026
f4a45bc
refactor(bt-service): Remove pointless compatibility layer with previ…
blacklight Jul 6, 2026
4fca884
feat(wear): Apply the same sorting as the phone app.
blacklight Jul 6, 2026
c3511d8
feat(wear): Default Wear OS sync to off; move permission flow to Sett…
blacklight Jul 6, 2026
08f352b
fix(wear): Use LocalActivity instead of LocalContext cast in CardView…
blacklight Jul 6, 2026
c53e05d
feat(wear): Surface distinct Bluetooth error states to the user
blacklight Jul 6, 2026
0a4374a
fix(wear): surface card parse failures to the user instead of silentl…
blacklight Jul 6, 2026
eb155f9
chore(wear): Replaced deprecated SharedPreferences usage
blacklight Jul 6, 2026
fc1c43d
chore(gitignore): Ignore /wear/release
blacklight Jul 6, 2026
ee33165
ci(wear): Add WearOS CI workflow for automatic build and LINT
blacklight Jul 13, 2026
2eae0bf
Update wear/src/main/res/values/strings.xml
blacklight Jul 13, 2026
5f74d07
fix(wear): paginate BT card sync to prevent timeout with many cards
blacklight Jul 14, 2026
523938d
chore(wear): Set `Catima Debug` app_name for `wear`, like it's done f…
blacklight Jul 14, 2026
1c47f85
refactor(wear): Replace boolean status flags with SyncStatus enum
blacklight Jul 14, 2026
c877568
perf(wear): add stable key to card list items
blacklight Jul 14, 2026
80ed360
chore: Replaced absolute import
blacklight Jul 14, 2026
451ffa1
fix: Reset Wear card screen brightness override on dispose
blacklight Jul 14, 2026
7e7aa8a
refactor(wear): use theme background and alias ComposeColor in CardVi…
blacklight Jul 14, 2026
603db47
chore(wear): Remove Java 21 compileOptions from Wear Gradle config
blacklight Jul 14, 2026
703e895
chore(ci): Removed unnecessary echo
blacklight Jul 14, 2026
3a5b02c
refactor(wearos): Remove legacy full cards request command
blacklight Jul 14, 2026
2069eb0
Update wear/src/main/AndroidManifest.xml
blacklight Jul 14, 2026
65cd6c4
Update wear/build.gradle.kts
blacklight Jul 14, 2026
ea22356
feat(wearos): Negotiate protocol via /VERSIONS command
blacklight Jul 14, 2026
fb0f797
fix(wear): Request BLUETOOTH_CONNECT when Wear sync is enabled
blacklight Jul 15, 2026
884b863
Update gradle/libs.versions.toml
blacklight Jul 15, 2026
825fbbc
fix(wear): Distinguish card sync parse failures
blacklight Jul 15, 2026
52a31e4
feat(wear): Add shared Bluetooth protocol and permission handling
blacklight Jul 15, 2026
531a9eb
Merge branch 'main' into feat/wearos
blacklight Jul 15, 2026
49a8e6d
refactor(wearos): centralize sync service lifecycle management
blacklight Jul 15, 2026
d6eeb30
refactor(wear): Replace FetchStatus with shared SyncStatus
blacklight Jul 15, 2026
2c69711
fix(gradle): Don't downgrade compose
blacklight Jul 18, 2026
193c001
fix(manifest): Moved standalone metadata under the right section.
blacklight Jul 18, 2026
6fa800d
fix(wearos): disable Wear OS sync toggle when Bluetooth permission is…
blacklight Jul 18, 2026
c64c3b8
fix(wear): Snackbar to guide users to grant sync permission
blacklight Jul 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/wear.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Android Wear CI
on:
workflow_dispatch:
push:
branches:
- main
- staging
- trying
pull_request:
branches:
- main
permissions:
actions: none
checks: none
contents: read
deployments: none
discussions: none
id-token: none
issues: none
packages: none
pages: none
pull-requests: none
repository-projects: none
security-events: none
statuses: none
env:
JAVA_HOME: /usr/lib/jvm/java-25-openjdk-amd64
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.0
- uses: gradle/actions/wrapper-validation@v6.2.0
- name: set up OpenJDK 25
run: |
sudo apt-get update
sudo apt-get install -y openjdk-25-jdk-headless
sudo update-alternatives --auto java
- name: Build
run: ./gradlew :wear:assembleRelease
- name: Check for non-free libraries
run: |
wget -q https://codeberg.org/Katastima/apkscanner/releases/download/v0.0.7/apk-scanner_v0.0.7.jar
java -jar apk-scanner_v0.0.7.jar scan-apk 'wear/build/outputs/apk/release/wear-release-unsigned.apk'
- name: Check lint
run: ./gradlew :wear:lintRelease
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
/app/*.log
/app/build
/app/release
/wear/build
Comment thread
blacklight marked this conversation as resolved.
/wear/release
/.idea/*
!/.idea/icon.svg
# Bundle
Expand All @@ -28,3 +30,4 @@

# Catima-specific
SHA256SUMS
/shared/build
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ android {
}

dependencies {
implementation(project(":shared"))

// AndroidX
implementation(libs.androidx.appcompat.appcompat)
implementation(libs.androidx.constraintlayout.constraintlayout)
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="23" />

<uses-feature
Expand Down Expand Up @@ -226,5 +231,10 @@
<action android:name="android.service.controls.ControlsProviderService" />
</intent-filter>
</service>

<service
android:name=".wearos.BluetoothServerService"
android:exported="false"
android:foregroundServiceType="connectedDevice" />
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package protect.card_locker;

import android.app.Application;
import android.content.Intent;

import androidx.appcompat.app.AppCompatDelegate;

import org.acra.ACRA;
import org.acra.config.CoreConfigurationBuilder;
import org.acra.config.DialogConfigurationBuilder;
import org.acra.config.MailSenderConfigurationBuilder;
import org.acra.data.StringFormat;

import protect.card_locker.preferences.Settings;
import protect.card_locker.wearos.WearSyncServiceManager;

public class LoyaltyCardLockerApplication extends Application {

Expand Down Expand Up @@ -41,5 +42,10 @@ public void onCreate() {
// Set theme
Settings settings = new Settings(this);
AppCompatDelegate.setDefaultNightMode(settings.getTheme());

// Start Bluetooth server for Wear OS companion if enabled.
// The service checks BLUETOOTH_CONNECT itself and stops if the permission is missing.
// The permission is requested from the launcher Activity when the UI resumes.
WearSyncServiceManager.INSTANCE.synchronize(this, null);
}
}
21 changes: 9 additions & 12 deletions app/src/main/java/protect/card_locker/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import protect.card_locker.databinding.MainActivityBinding
import protect.card_locker.databinding.SortingOptionBinding
import protect.card_locker.preferences.Settings
import protect.card_locker.preferences.SettingsActivity
import protect.card_locker.wearos.WearSyncPermissionRequester
import java.io.UnsupportedEncodingException
import java.util.concurrent.atomic.AtomicInteger
import androidx.core.content.edit
Expand All @@ -61,6 +62,7 @@ class MainActivity : CatimaAppCompatActivity(), CardAdapterListener {
private lateinit var mUpdateLoyaltyCardListRunnable: Runnable
private lateinit var mBarcodeScannerLauncher: ActivityResultLauncher<Intent>
private lateinit var mSettingsLauncher: ActivityResultLauncher<Intent>
private lateinit var mWearSyncPermissionRequester: WearSyncPermissionRequester

private val mCurrentActionModeCallback: ActionMode.Callback = object : ActionMode.Callback {
override fun onCreateActionMode(inputMode: ActionMode, inputMenu: Menu?): Boolean {
Expand Down Expand Up @@ -318,6 +320,8 @@ class MainActivity : CatimaAppCompatActivity(), CardAdapterListener {
}
}

mWearSyncPermissionRequester = WearSyncPermissionRequester(this, this)

onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (mSearchView != null && !mSearchView!!.isIconified) {
Expand Down Expand Up @@ -394,6 +398,8 @@ class MainActivity : CatimaAppCompatActivity(), CardAdapterListener {
val settings = Settings(this)
layoutManager.setSpanCount(settings.getPreferredColumnCount())
}

mWearSyncPermissionRequester.synchronize()
}

private fun displayCardSetupOptions(menu: Menu, shouldShow: Boolean) {
Expand Down Expand Up @@ -467,11 +473,7 @@ class MainActivity : CatimaAppCompatActivity(), CardAdapterListener {
ShortcutHelper.updateShortcuts(mAdapter.mContext)
}

private fun processParseResultList(
parseResultList: MutableList<ParseResult?>,
group: String?,
closeAppOnNoBarcode: Boolean
) {
private fun processParseResultList(parseResultList: MutableList<ParseResult?>) {
require(!parseResultList.isEmpty()) { "parseResultList may not be empty" }

Utils.makeUserChooseParseResultFromList(
Expand All @@ -482,17 +484,12 @@ class MainActivity : CatimaAppCompatActivity(), CardAdapterListener {
val intent =
Intent(applicationContext, LoyaltyCardEditActivity::class.java)
val bundle = parseResult.toLoyaltyCardBundle(this@MainActivity)
if (group != null) {
bundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, group)
}
intent.putExtras(bundle)
startActivity(intent)
}

override fun onUserDismissedSelector() {
if (closeAppOnNoBarcode) {
finish()
}
finish()
}
})
}
Expand Down Expand Up @@ -556,7 +553,7 @@ class MainActivity : CatimaAppCompatActivity(), CardAdapterListener {
return
}

processParseResultList(parseResultList, null, true)
processParseResultList(parseResultList)
}

private fun extractIntentFields(intent: Intent) {
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/protect/card_locker/NotificationInfo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package protect.card_locker

object NotificationInfo {
object WearBluetooth {
const val NOTIFICATION_ID = 1001
const val CHANNEL_ID = "catima_wear_bt"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,8 @@ public int getPreferredColumnCount() {
public boolean useVolumeKeysForNavigation() {
return getBoolean(R.string.settings_key_use_volume_keys_navigation, false);
}

public boolean getWearSyncEnabled() {
return getBoolean(R.string.settings_key_wear_sync, false);
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
package protect.card_locker.preferences

import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.view.MenuItem
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import com.google.android.material.snackbar.Snackbar
import protect.card_locker.BuildConfig
import protect.card_locker.CatimaAppCompatActivity
import protect.card_locker.MainActivity
import protect.card_locker.R
import protect.card_locker.Utils
import protect.card_locker.databinding.SettingsActivityBinding
import protect.card_locker.wearos.WearSyncPermissionRequester

class SettingsActivity : CatimaAppCompatActivity() {

Expand Down Expand Up @@ -81,7 +85,16 @@ class SettingsActivity : CatimaAppCompatActivity() {
class SettingsFragment : PreferenceFragmentCompat() {
var mReloadMain: Boolean = false

private lateinit var wearSyncPermissionRequester: WearSyncPermissionRequester

override fun onResume() {
super.onResume()
wearSyncPermissionRequester.synchronize()
}

override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
wearSyncPermissionRequester = WearSyncPermissionRequester(this, requireContext())

// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preferences)

Expand All @@ -104,7 +117,7 @@ class SettingsActivity : CatimaAppCompatActivity() {

val oledDarkPreference = findPreference<Preference>(getString(R.string.settings_key_oled_dark))
oledDarkPreference!!.setOnPreferenceChangeListener { _, _ ->
refreshActivity(true)
refreshActivity()
true
}

Expand Down Expand Up @@ -148,7 +161,7 @@ class SettingsActivity : CatimaAppCompatActivity() {
localePreference.setOnPreferenceChangeListener { _, newValue ->
// See corresponding comment in Utils.updateBaseContextLocale for Android 6- notes
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
refreshActivity(true)
refreshActivity()
return@setOnPreferenceChangeListener true
}
val newLocale = newValue as String
Expand All @@ -157,13 +170,46 @@ class SettingsActivity : CatimaAppCompatActivity() {
true
}

val wearSyncPreference = findPreference<SwitchPreferenceCompat>(getString(R.string.settings_key_wear_sync))
wearSyncPreference!!.setOnPreferenceChangeListener { preference, newValue ->
val enabled = newValue as Boolean
if (enabled) {
wearSyncPermissionRequester.onWearSyncChanged(true) { granted ->
if (granted) {
(preference as? SwitchPreferenceCompat)?.isChecked = true
} else {
showWearSyncPermissionDeniedSnackbar()
}
}
false
} else {
wearSyncPermissionRequester.onWearSyncChanged(false)
true
}
}

Comment thread
TheLastProject marked this conversation as resolved.
// Hide crash reporter settings on builds it's not enabled on
val crashReporterPreference = findPreference<Preference>("acra.enable")
crashReporterPreference!!.isVisible = BuildConfig.useAcraCrashReporter
}

private fun refreshActivity(reloadMain: Boolean) {
mReloadMain = reloadMain || mReloadMain
private fun showWearSyncPermissionDeniedSnackbar() {
Snackbar.make(
requireView(),
R.string.wear_sync_permission_required,
Snackbar.LENGTH_LONG
).setAction(R.string.open_settings) {
startActivity(
Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", requireContext().packageName, null)
)
)
}.show()
}

private fun refreshActivity() {
mReloadMain = true
activity?.recreate()
}
}
Expand Down
Loading