Compare commits

..
Author SHA1 Message Date
Sylvia van Os 22193b1872 Release Catima v2.45.0 2026-08-12 18:56:26 +02:00
Sylvia van Os 306e7b5201 Release Catima Wear OS v1.0.0 2026-08-12 18:46:49 +02:00
Sylvia van Os 0c8104af96 Merge pull request #3260 from CatimaLoyalty/create-pull-request/patch-1786549695
Update Fastlane changelogs
2026-08-12 18:40:58 +02:00
TheLastProject 3b06f7a105 Update Fastlane changelogs 2026-08-12 15:48:15 +00:00
Sylvia van Os 0869623c1d Merge pull request #3218 from CatimaLoyalty/wip/wearos
WearOS support
2026-08-12 17:48:01 +02:00
Sylvia van Os bfa79a6f68 Notify on foreground service start failure 2026-08-11 22:06:01 +02:00
Sylvia van Os cda842485f Update privacy policy 2026-08-11 22:06:01 +02:00
Sylvia van Os db35306a26 Add WearOS screenshots and description change 2026-08-11 22:06:01 +02:00
Sylvia van Os 2363861a62 Fail pipeline on bad libraries 2026-08-11 22:06:01 +02:00
Sylvia van Os 2960585250 Prepare new versioning and CHANGELOG structure 2026-08-11 22:06:01 +02:00
Sylvia van Os ff830ee50d Improve notification icons
This improves the sizing of the notification icons and uses a generic
sync icon for the Bluetooth server running (so users don't get used to
ignoring the Catima icon).

Generated throudh Android Studio New -> Image Asset -> Notification
Icons -> Path: .design/master_monochrome.svg -> Trim: Yes -> Padding: 0%
2026-08-11 22:06:01 +02:00
Sylvia van Os 3c917da56a Modify code to support minor API versions 2026-08-11 22:06:01 +02:00
Sylvia van Os 6ea1d9ec73 Fix some documentation mistakes for the Content Provider 2026-08-11 22:06:01 +02:00
Sylvia van Os 5ebd7cbb63 Add documentation for Bluetooth server 2026-08-11 22:06:01 +02:00
Sylvia van Os 0d91444ee2 Consistently use Wear OS naming 2026-08-11 22:06:01 +02:00
Sylvia van Os 9459c78e7c Migrate WearOS app to Material3
Also always display sync status on screen
2026-08-11 22:06:01 +02:00
Fabio ManganielloandSylvia van Os b326a7148d feat(bluetooth): Add trusted device pairing (#3223)
Co-authored-by: Sylvia van Os <sylvia@hackerchick.me>
2026-08-11 22:06:01 +02:00
Sylvia van Os 5ae850dbe8 Restructure smartwatch support settings 2026-08-11 22:06:01 +02:00
Fabio ManganielloandSylvia van Os 2ef9c9c68e Add WearOS companion app (#3185)
Co-authored-by: Sylvia van Os <sylvia@hackerchick.me>
2026-08-11 22:06:01 +02:00
Sylvia van Os 613bd000d2 Merge pull request #3256 from CatimaLoyalty/create-pull-request/patch-1786251663
Update contributors
2026-08-09 10:37:08 +02:00
TheLastProject f7ea5bcb7b Update contributors 2026-08-09 05:01:02 +00:00
Sylvia van Os b8b5b8ae9b Merge pull request #3252 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2026-08-06 12:02:55 +02:00
Sylvia van Os e6085a64be Merge pull request #3253 from CatimaLoyalty/dependabot/gradle/androidx.constraintlayout-constraintlayout-2.2.2
Bump androidx.constraintlayout:constraintlayout from 2.2.1 to 2.2.2
2026-08-06 08:08:45 +02:00
dependabot[bot] e77acf9b1e Bump androidx.constraintlayout:constraintlayout from 2.2.1 to 2.2.2
Bumps androidx.constraintlayout:constraintlayout from 2.2.1 to 2.2.2.

---
updated-dependencies:
- dependency-name: androidx.constraintlayout:constraintlayout
  dependency-version: 2.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-06 02:53:46 +00:00
Joel A dc4299d315 Translated using Weblate (Swedish)
Currently translated at 100.0% (324 of 324 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/sv/
2026-08-06 04:04:57 +02:00
Damjan Gerl 1c8d2ba19e Translated using Weblate (Slovenian)
Currently translated at 100.0% (166 of 166 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/sl/
2026-08-06 04:04:56 +02:00
NooB9496 d46455c021 Translated using Weblate (Polish)
Currently translated at 100.0% (166 of 166 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/pl/
2026-08-06 04:04:53 +02:00
Watchman89 abd4c24138 Translated using Weblate (Dutch)
Currently translated at 100.0% (166 of 166 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/nl/
2026-08-06 04:04:53 +02:00
NooB9496 1e922ae074 Translated using Weblate (Polish)
Currently translated at 100.0% (324 of 324 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/pl/
2026-08-06 04:04:47 +02:00
Watchman89 335059a681 Translated using Weblate (Dutch)
Currently translated at 100.0% (324 of 324 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/nl/
2026-08-06 04:04:45 +02:00
Sylvia van Os a530991119 Merge pull request #3248 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2026-08-05 18:12:01 +02:00
Edgars Andersons 45130b5f85 Translated using Weblate (Latvian)
Currently translated at 100.0% (324 of 324 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/lv/
2026-08-05 00:03:39 +02:00
Damjan Gerl 70fd411640 Translated using Weblate (Slovenian)
Currently translated at 94.5% (157 of 166 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/sl/
2026-08-05 00:03:37 +02:00
Sylvia van Os 4d47cd4a4a Merge pull request #3246 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2026-08-03 19:46:26 +02:00
Sylvia van Os bb694ca9cb Translated using Weblate (Arabic)
Currently translated at 98.4% (319 of 324 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/ar/
2026-08-03 17:21:05 +02:00
Damjan Gerl 7ede8424d0 Translated using Weblate (Slovenian)
Currently translated at 78.9% (131 of 166 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/sl/
2026-08-03 17:21:04 +02:00
Sylvia van Os 1167cf51b5 Translated using Weblate (Lithuanian)
Currently translated at 8.4% (14 of 166 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/lt/
2026-08-03 17:21:01 +02:00
Eduardo Augustinho 13d2c81c5c Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.3% (165 of 166 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/pt_BR/
2026-08-02 15:05:14 +00:00
Eduardo Augustinho 102a34a3f3 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (324 of 324 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/pt_BR/
2026-08-02 15:05:12 +00:00
E Ta 0c490f2bdb Translated using Weblate (Arabic)
Currently translated at 98.4% (319 of 324 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/ar/
2026-08-02 15:05:11 +00:00
Zeet Baral 8f69f9cef9 Translated using Weblate (Bengali (India))
Currently translated at 62.0% (201 of 324 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/bn_IN/
2026-08-02 15:05:09 +00:00
109247019824 00e65a39de Translated using Weblate (Bulgarian)
Currently translated at 100.0% (324 of 324 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/bg/
2026-08-02 15:05:08 +00:00
Damjan Gerl 8698c6099c Translated using Weblate (Slovenian)
Currently translated at 64.4% (107 of 166 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/sl/
2026-08-02 15:05:07 +00:00
dukezas da3fbdb401 Translated using Weblate (Lithuanian)
Currently translated at 8.4% (14 of 166 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/lt/
2026-08-02 15:05:06 +00:00
Nik b28052c46c Translated using Weblate (Italian)
Currently translated at 100.0% (166 of 166 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/it/
2026-08-02 15:05:04 +00:00
dukezas b438a7f03d Translated using Weblate (Lithuanian)
Currently translated at 83.9% (272 of 324 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/lt/
2026-08-02 15:05:03 +00:00
Nik c144035d95 Translated using Weblate (Italian)
Currently translated at 100.0% (324 of 324 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/it/
2026-08-02 15:05:02 +00:00
Sylvia van Os 7658c3980a Merge pull request #3245 from CatimaLoyalty/create-pull-request/patch-1785652221
Update contributors
2026-08-02 10:03:34 +02:00
TheLastProject c11297a733 Update contributors 2026-08-02 06:30:20 +00:00
Sylvia van Os c89e60adf6 Merge pull request #3239 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2026-07-31 17:07:41 +02:00
josé m. fa33f61183 Translated using Weblate (Galician)
Currently translated at 100.0% (324 of 324 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/gl/
2026-07-31 03:02:41 +00:00
bruh f3e57d816c Translated using Weblate (Vietnamese)
Currently translated at 100.0% (324 of 324 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/vi/
2026-07-31 03:02:39 +00:00
Fjuro bf2e1eface Translated using Weblate (Czech)
Currently translated at 100.0% (166 of 166 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/cs/
2026-07-31 03:02:37 +00:00
Fjuro a69fff9b53 Translated using Weblate (Czech)
Currently translated at 100.0% (324 of 324 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/cs/
2026-07-31 03:02:34 +00:00
Sylvia van Os 9fc0038bd5 Merge pull request #3238 from CatimaLoyalty/dependabot/github_actions/actions/setup-python-7.0.0
Bump actions/setup-python from 6.3.0 to 7.0.0
2026-07-30 08:21:46 +02:00
dependabot[bot] ffd8dd5633 Bump actions/setup-python from 6.3.0 to 7.0.0
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.3.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v6.3.0...v7.0.0)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-30 06:20:46 +00:00
Sylvia van Os c3947b0f6a Merge pull request #3237 from CatimaLoyalty/dependabot/github_actions/actions/checkout-7.0.1
Bump actions/checkout from 7.0.0 to 7.0.1
2026-07-30 08:19:02 +02:00
dependabot[bot] 84b17b17fe Bump actions/checkout from 7.0.0 to 7.0.1
Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v7.0.0...v7.0.1)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-30 02:52:41 +00:00
Sylvia van Os 3956a1123e Merge pull request #3236 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2026-07-29 20:55:24 +02:00
Sylvia van Os f04185f6fd Merge pull request #2580 from CatimaLoyalty/fix/weblate_missingquantity_fix
Re-enable missingQuantity lint checks
2026-07-29 20:48:07 +02:00
Sylvia van Os 10d188ee40 Re-enable missingQuantity lint checks where possible
https://github.com/WeblateOrg/weblate/issues/7520
2026-07-29 20:32:12 +02:00
Priit Jõerüüt 63e2d3740f Translated using Weblate (Estonian)
Currently translated at 100.0% (324 of 324 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/et/
2026-07-29 20:04:49 +02:00
Damjan Gerl 672173437f Translated using Weblate (Slovenian)
Currently translated at 64.4% (107 of 166 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/sl/
2026-07-29 20:04:46 +02:00
B o d o 2c851158f5 Translated using Weblate (German)
Currently translated at 100.0% (166 of 166 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/de/
2026-07-29 20:04:38 +02:00
Sylvain Pichon 367733676a Translated using Weblate (French)
Currently translated at 100.0% (166 of 166 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/fr/
2026-07-29 20:04:38 +02:00
Damjan Gerl 538cd2078b Translated using Weblate (Slovenian)
Currently translated at 100.0% (324 of 324 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/sl/
2026-07-29 20:04:37 +02:00
Sylvia van Os 731833d9be Release Catima 2.44.0 2026-07-28 18:21:59 +02:00
Sylvia van Os 7ebd71283f Merge pull request #3234 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2026-07-28 18:21:16 +02:00
Oğuz Ersen 5eb1942ad2 Translated using Weblate (Turkish)
Currently translated at 100.0% (324 of 324 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/tr/
2026-07-28 18:07:24 +02:00
Jiri Grönroos 999222f3a4 Translated using Weblate (Finnish)
Currently translated at 99.3% (322 of 324 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/fi/
2026-07-28 18:07:24 +02:00
Hosted Weblate user 54392 e82d726898 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (166 of 166 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/zh_Hans/
2026-07-28 18:07:21 +02:00
Hosted Weblate user 54392 0f48c75512 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (324 of 324 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/zh_Hans/
2026-07-28 18:07:20 +02:00
Liner Seven 183d3ea666 Translated using Weblate (Japanese)
Currently translated at 100.0% (166 of 166 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/ja/
2026-07-28 18:07:20 +02:00
Liner Seven 4e4e9123f2 Translated using Weblate (Japanese)
Currently translated at 100.0% (324 of 324 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/ja/
2026-07-28 18:07:19 +02:00
solokot c09566d38a Translated using Weblate (Russian)
Currently translated at 100.0% (166 of 166 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/ru/
2026-07-28 18:07:19 +02:00
B o d o 750ac6f2ec Translated using Weblate (German)
Currently translated at 100.0% (166 of 166 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/de/
2026-07-28 18:07:18 +02:00
solokot 1ed78fa680 Translated using Weblate (Russian)
Currently translated at 100.0% (324 of 324 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/ru/
2026-07-28 18:07:18 +02:00
Sylvain Pichon 48a26cf1f7 Translated using Weblate (French)
Currently translated at 100.0% (324 of 324 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/fr/
2026-07-28 18:07:17 +02:00
Vasilis K. bc6a63a1f0 Translated using Weblate (Greek)
Currently translated at 100.0% (324 of 324 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/el/
2026-07-28 18:07:16 +02:00
B o d o 45f360904f Translated using Weblate (German)
Currently translated at 100.0% (324 of 324 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/de/
2026-07-28 18:07:16 +02:00
Sylvia van Os bdf39a1dd7 Merge pull request #3233 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2026-07-28 10:49:51 +02:00
Ricardo Xavier abff198d01 Translated using Weblate (Portuguese)
Currently translated at 99.6% (322 of 323 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/pt/
2026-07-27 23:03:51 +00:00
Watchman89 6721ecf113 Translated using Weblate (Dutch)
Currently translated at 100.0% (165 of 165 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/nl/
2026-07-27 23:03:47 +00:00
Danny Bouwers 3899c71317 Translated using Weblate (Dutch)
Currently translated at 100.0% (323 of 323 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/nl/
2026-07-27 23:03:44 +00:00
Sylvia van Os b8e4ec9c9f Merge pull request #3232 from CatimaLoyalty/create-pull-request/patch-1785086352
Update Fastlane changelogs
2026-07-26 19:19:34 +02:00
TheLastProject 92cb120dd0 Update Fastlane changelogs 2026-07-26 17:19:12 +00:00
Sylvia van Os 7a8f9c38eb Update CHANGELOG 2026-07-26 19:18:58 +02:00
KamiDaga dc7498e505 Merge pull request #3219 from KamiDaga/feature/overflow-menu-to-image-gallery
Added overflow menu to open card image in image gallery
2026-07-26 19:18:00 +02:00
Sylvia van Os 02e925f77a Merge pull request #3231 from CatimaLoyalty/create-pull-request/patch-1785047585
Update contributors
2026-07-26 09:51:28 +02:00
TheLastProject 7dc2a479b6 Update contributors 2026-07-26 06:33:05 +00:00
Sylvia van Os 1913d629e7 Merge pull request #3230 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2026-07-25 22:34:09 +02:00
ssantos 59a750f41c Translated using Weblate (Portuguese)
Currently translated at 99.3% (321 of 323 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/pt/
2026-07-25 22:02:03 +02:00
ssantos 46dfafe0b2 Translated using Weblate (Portuguese (Portugal))
Currently translated at 99.3% (321 of 323 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/pt_PT/
2026-07-25 22:02:02 +02:00
Sylvia van Os 1faab05032 Merge pull request #3228 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2026-07-24 14:43:26 +02:00
Oğuz Ersen c895f0db56 Translated using Weblate (Turkish)
Currently translated at 100.0% (323 of 323 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/tr/
2026-07-24 11:02:09 +02:00
109247019824 c622df96f0 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (323 of 323 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/bg/
2026-07-24 11:02:08 +02:00
Sylvia van Os 30ac13ec73 Merge pull request #3227 from CatimaLoyalty/dependabot/gradle/com.android.application-9.3.1
Bump com.android.application from 9.3.0 to 9.3.1
2026-07-24 08:40:22 +02:00
dependabot[bot] efbe809982 Bump com.android.application from 9.3.0 to 9.3.1
Bumps com.android.application from 9.3.0 to 9.3.1.

---
updated-dependencies:
- dependency-name: com.android.application
  dependency-version: 9.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 02:53:57 +00:00
Sylvia van Os 92df5dd36a Merge pull request #3226 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2026-07-23 10:25:00 +02:00
Rizqi Putra ebdf13678a Translated using Weblate (Indonesian)
Currently translated at 95.1% (157 of 165 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/id/
2026-07-23 08:02:09 +00:00
Rizqi Putra 0a0d46033c Translated using Weblate (Indonesian)
Currently translated at 100.0% (323 of 323 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/id/
2026-07-23 08:02:08 +00:00
solokot 7f72565e24 Translated using Weblate (Russian)
Currently translated at 100.0% (165 of 165 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/ru/
2026-07-23 08:02:08 +00:00
Sylvia van Os cfcf1e2fcb Merge pull request #3224 from CatimaLoyalty/dependabot/gradle/kotlin-2.4.10
Bump kotlin from 2.4.0 to 2.4.10
2026-07-22 06:31:37 +02:00
dependabot[bot] faf2f7a38c Bump kotlin from 2.4.0 to 2.4.10
Bumps `kotlin` from 2.4.0 to 2.4.10.

Updates `org.jetbrains.kotlin.android` from 2.4.0 to 2.4.10
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v2.4.0...v2.4.10)

Updates `org.jetbrains.kotlin.plugin.compose` from 2.4.0 to 2.4.10
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v2.4.0...v2.4.10)

---
updated-dependencies:
- dependency-name: org.jetbrains.kotlin.android
  dependency-version: 2.4.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.jetbrains.kotlin.plugin.compose
  dependency-version: 2.4.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-22 02:57:36 +00:00
Sylvia van Os ace7e5abdd Merge pull request #3220 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2026-07-21 22:10:54 +02:00
Damjan Gerl f50e547980 Translated using Weblate (Slovenian)
Currently translated at 43.0% (71 of 165 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/sl/
2026-07-21 08:02:23 +00:00
KamiDaga c05a303568 Translated using Weblate (Italian)
Currently translated at 100.0% (165 of 165 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/it/
2026-07-21 08:02:22 +00:00
Damjan Gerl 4fcd41d3cc Translated using Weblate (Slovenian)
Currently translated at 100.0% (323 of 323 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/sl/
2026-07-21 08:02:22 +00:00
KamiDaga 3e6c4c1b13 Translated using Weblate (Italian)
Currently translated at 100.0% (323 of 323 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/it/
2026-07-21 08:02:21 +00:00
Sylvia van Os 55d356ea40 Merge pull request #3217 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2026-07-20 18:15:52 +02:00
Libre a752421efe Translated using Weblate (Spanish)
Currently translated at 95.7% (158 of 165 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/es/
2026-07-19 21:02:19 +00:00
NooB9496 aa4b41612f Translated using Weblate (Polish)
Currently translated at 100.0% (165 of 165 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/pl/
2026-07-19 21:02:18 +00:00
Fjuro f576228c2d Translated using Weblate (Czech)
Currently translated at 100.0% (165 of 165 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/cs/
2026-07-19 21:02:17 +00:00
NooB9496 c93add3b83 Translated using Weblate (Polish)
Currently translated at 100.0% (323 of 323 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/pl/
2026-07-19 21:02:17 +00:00
Libre 117446cebb Translated using Weblate (Spanish)
Currently translated at 100.0% (323 of 323 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/es/
2026-07-19 21:02:16 +00:00
Fjuro 4bee5ec6c8 Translated using Weblate (Czech)
Currently translated at 100.0% (323 of 323 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/cs/
2026-07-19 21:02:16 +00:00
Sylvia van Os 92776c9e8d Merge pull request #3215 from CatimaLoyalty/create-pull-request/patch-1784442491
Update contributors
2026-07-19 10:44:47 +02:00
TheLastProject eea3fb5c54 Update contributors 2026-07-19 06:28:11 +00:00
Sylvia van Os dbd0727af2 Merge pull request #3213 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2026-07-17 22:21:16 +02:00
josé m. a4cf49754a Translated using Weblate (Galician)
Currently translated at 100.0% (323 of 323 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/gl/
2026-07-17 22:02:29 +02:00
Priit Jõerüüt 587c5991f1 Translated using Weblate (Estonian)
Currently translated at 100.0% (323 of 323 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/et/
2026-07-17 22:02:28 +02:00
Gabriel 10234ffcef Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (165 of 165 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/pt_BR/
2026-07-17 22:02:27 +02:00
Gabriel 2a2710e391 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (323 of 323 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/pt_BR/
2026-07-17 22:02:03 +02:00
Joel A f1b5423d94 Translated using Weblate (Swedish)
Currently translated at 99.0% (320 of 323 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/sv/
2026-07-17 22:02:02 +02:00
Hosted Weblate user 54392 1cb916d16a Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (165 of 165 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/zh_Hans/
2026-07-17 22:01:59 +02:00
Hosted Weblate user 54392 6396120fb0 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (323 of 323 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/zh_Hans/
2026-07-17 22:01:53 +02:00
Liner Seven 2fe5157519 Translated using Weblate (Japanese)
Currently translated at 100.0% (165 of 165 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/ja/
2026-07-17 22:01:52 +02:00
Liner Seven 4ce480defb Translated using Weblate (Japanese)
Currently translated at 100.0% (323 of 323 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/ja/
2026-07-17 22:01:44 +02:00
B o d o 3ea5d16f19 Translated using Weblate (German)
Currently translated at 100.0% (165 of 165 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/de/
2026-07-17 22:01:42 +02:00
Sylvain Pichon 31fc58576c Translated using Weblate (French)
Currently translated at 100.0% (165 of 165 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/fr/
2026-07-17 22:01:35 +02:00
solokot 9573799eba Translated using Weblate (Russian)
Currently translated at 100.0% (323 of 323 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/ru/
2026-07-17 22:01:27 +02:00
Sylvain Pichon df94e94b31 Translated using Weblate (French)
Currently translated at 100.0% (323 of 323 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/fr/
2026-07-17 22:01:25 +02:00
Vasilis K. 98b561b186 Translated using Weblate (Greek)
Currently translated at 100.0% (323 of 323 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/el/
2026-07-17 22:01:24 +02:00
B o d o 8ee469343c Translated using Weblate (German)
Currently translated at 100.0% (323 of 323 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/de/
2026-07-17 22:01:22 +02:00
Sylvia van Os e54d129577 Merge pull request #3210 from CatimaLoyalty/dependabot/github_actions/ReactiveCircus/android-emulator-runner-2.38.0
Bump ReactiveCircus/android-emulator-runner from 2.37.0 to 2.38.0
2026-07-16 08:21:01 +02:00
dependabot[bot] a7a3237f4f Bump ReactiveCircus/android-emulator-runner from 2.37.0 to 2.38.0
Bumps [ReactiveCircus/android-emulator-runner](https://github.com/reactivecircus/android-emulator-runner) from 2.37.0 to 2.38.0.
- [Release notes](https://github.com/reactivecircus/android-emulator-runner/releases)
- [Changelog](https://github.com/ReactiveCircus/android-emulator-runner/blob/main/CHANGELOG.md)
- [Commits](https://github.com/reactivecircus/android-emulator-runner/compare/v2.37.0...v2.38.0)

---
updated-dependencies:
- dependency-name: ReactiveCircus/android-emulator-runner
  dependency-version: 2.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-16 02:53:16 +00:00
432 changed files with 3924 additions and 204 deletions

No files matched your search

+27 -16
View File
@@ -4,11 +4,7 @@ on:
push:
branches:
- main
- staging
- trying
pull_request:
branches:
- main
permissions:
actions: none
checks: none
@@ -30,9 +26,13 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
flavor: [Foss, Gplay]
flavor: [fdroidLegacy, foss, gplay]
steps:
- uses: actions/checkout@v7.0.0
- uses: actions/checkout@v7.0.1
- name: Ensure sentence-cased flavor variables
run: |
flavor="$(echo ${{ matrix.flavor }})"
echo "sentenceCasedFlavor=${flavor^}" >> "$GITHUB_ENV"
- uses: gradle/actions/wrapper-validation@v6.2.0
- name: set up OpenJDK 25
run: |
@@ -40,35 +40,46 @@ jobs:
sudo apt-get install -y openjdk-25-jdk-headless
sudo update-alternatives --auto java
- name: Build
run: ./gradlew assemble${{ matrix.flavor }}Release
run: ./gradlew assemble${sentenceCasedFlavor}Release
- 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 "$(echo 'app/build/outputs/apk/${{ matrix.flavor }}/release/app-${{ matrix.flavor }}-release-unsigned.apk' | tr '[:upper:]' '[:lower:]')"
# Get Katastima APK scanner
wget -q https://codeberg.org/Katastima/apkscanner/releases/download/v0.0.9/apk-scanner_v0.0.9.jar
# Print pretty result
java -jar apk-scanner_v0.0.9.jar scan-apk "$(echo 'app/build/outputs/apk/${{ matrix.flavor }}/release/app-${{ matrix.flavor }}-release-unsigned.apk')"
# Check for offending libraries
# Exit code 4 = no result, which is what we want
set +e
java -jar apk-scanner_v0.0.9.jar scan-apk --json=yes "$(echo 'app/build/outputs/apk/${{ matrix.flavor }}/release/app-${{ matrix.flavor }}-release-unsigned.apk')" | jq --exit-status '.libraryCheckResult.offendingLibraries[] | select((.libraryId == "/org/acra" and .license == "Apache-2.0")|not)'
if [ "$?" -ne 4 ]; then
echo "Bad libraries found!"
exit 1
fi
set -e
- name: Check lint
run: ./gradlew lint${{ matrix.flavor }}Release
run: ./gradlew lint${sentenceCasedFlavor}Release
- name: Run unit tests
run: timeout 5m ./gradlew test${{ matrix.flavor }}DebugUnitTest || { ./gradlew --stop && timeout 5m ./gradlew test${{ matrix.flavor }}DebugUnitTest; }
run: timeout 5m ./gradlew test${sentenceCasedFlavor}DebugUnitTest || { ./gradlew --stop && timeout 5m ./gradlew test${sentenceCasedFlavor}DebugUnitTest; }
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Run instrumented tests (API 23)
uses: ReactiveCircus/android-emulator-runner@v2.37.0
uses: ReactiveCircus/android-emulator-runner@v2.38.0
with:
api-level: 23
arch: x86_64
script: ./gradlew connected${{ matrix.flavor }}DebugAndroidTest
script: ./gradlew connected${sentenceCasedFlavor}DebugAndroidTest
- name: Run instrumented tests (API 35)
uses: ReactiveCircus/android-emulator-runner@v2.37.0
uses: ReactiveCircus/android-emulator-runner@v2.38.0
with:
api-level: 35
arch: x86_64
script: ./gradlew connected${{ matrix.flavor }}DebugAndroidTest
script: ./gradlew connected${sentenceCasedFlavor}DebugAndroidTest
- name: Archive test results
if: always()
uses: actions/upload-artifact@v7.0.1
with:
name: test-results-flavor${{ matrix.flavor }}
name: test-results-flavor-${{ matrix.flavor }}
path: app/build/reports
+2 -2
View File
@@ -19,9 +19,9 @@ jobs:
steps:
- name: Checkout repo
id: checkout
uses: actions/checkout@v7.0.0
uses: actions/checkout@v7.0.1
- name: Setup Python
uses: actions/setup-python@v6.3.0
uses: actions/setup-python@v7.0.0
with:
python-version: '3.x'
- name: Run converter script
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
steps:
- name: Checkout repo
id: checkout
uses: actions/checkout@v7.0.0
uses: actions/checkout@v7.0.1
- name: Update contributors
id: update_contributors
uses: TheLastProject/contributors-to-file-action@v3.2.0
@@ -17,7 +17,7 @@ jobs:
generate-feature-graphic:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.0
- uses: actions/checkout@v7.0.1
- name: Install requirements
run: |
sudo apt-get update
+1 -5
View File
@@ -4,11 +4,7 @@ on:
push:
branches:
- main
- staging
- trying
pull_request:
branches:
- main
permissions:
actions: none
checks: none
@@ -27,7 +23,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.0
- uses: actions/checkout@v7.0.1
- name: Fail on bad translations
run: if grep -ri "&lt;xliff" app/src/main/res/values*/strings.xml; then echo "Invalidly escaped translations found"; exit 1; fi
- name: Check app_name consistency
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
update-locales:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.0
- uses: actions/checkout@v7.0.1
- name: Add new locales
run: .scripts/new-locales.py
- name: Update locales
+53
View File
@@ -0,0 +1,53 @@
name: Android Wear CI
on:
workflow_dispatch:
push:
branches:
- main
pull_request:
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: |
# Get Katastima APK scanner
wget -q https://codeberg.org/Katastima/apkscanner/releases/download/v0.0.9/apk-scanner_v0.0.9.jar
# Print pretty result
java -jar apk-scanner_v0.0.9.jar scan-apk "wear/build/outputs/apk/release/wear-release-unsigned.apk"
# Check for offending libraries
# Exit code 4 = no result, which is what we want
set +e
java -jar apk-scanner_v0.0.9.jar scan-apk --json=yes "wear/build/outputs/apk/release/wear-release-unsigned.apk" | jq --exit-status '.libraryCheckResult.offendingLibraries[]'
if [ "$?" -ne 4 ]; then
echo "Bad libraries found!"
exit 1
fi
set -e
- name: Check lint
run: ./gradlew :wear:lintRelease
+3
View File
@@ -19,6 +19,8 @@
/app/*.log
/app/build
/app/release
/wear/build
/wear/release
/.idea/*
!/.idea/icon.svg
# Bundle
@@ -28,3 +30,4 @@
# Catima-specific
SHA256SUMS
/shared/build
+1 -1
View File
@@ -15,7 +15,7 @@ with open('CHANGELOG.md') as changelog:
changelogs[version_code] = text
text = []
match = re.match("## \S* - (\d*).*", line)
match = re.match("## .+ - (\d*).*", line)
if not match:
raise ValueError(f"Invalid version line: {line}")
version_code = match.group(1)
+13
View File
@@ -1,5 +1,18 @@
# Changelog
## v2.45.0 (Android) - 1002
- Add a Bluetooth server for Wear OS companion app support
- Remove embedded CHANGELOG
## v1.0.0 (Wear OS) - 200
- Initial release of Wear OS companion app
## v2.44.0 - 168 (2026-07-28)
- Add option to open image in internal image viewer in gallery
## v2.43.0 - 167 (2026-07-12)
- Add support for showing the card ID in the card list
+2 -1
View File
@@ -1,11 +1,12 @@
**Last updated**
April 29 2026
August 7 2026
# Privacy Policy
Catima does not collect or transmit any personal information.
To ensure correct app functionality, we require access to the following:
- Bluetooth: When using Catima together with the Wear OS companion app, Bluetooth access is needed to be able to communicate to Wear OS. This may show up as "find, connect to, and determine the relative position of nearby devices" because Android considers Bluetooth as part of location, but location information is never collected or used. The app can still be used when Bluetooth permission is denied, but you will not be able to use the Wear OS companion app.
- Camera: We need access to your camera to be able to scan barcodes. The app can still be used when camera access is denied, but you will have to manually type the barcode information.
- NFC: We need NFC access to temporarily disable NFC while showing a barcode to prevent NFC payment apps from triggering (such as when using a check-in gates which support both barcodes and NFC payments). This functionality can be disabled in settings.
- Storage (Android 5 and 6 only): We need access to your device storage to create or import backups. The app can still be used when storage access is denied, but you will not be able to create or import backups.
+14 -13
View File
@@ -13,20 +13,14 @@ kotlin {
android {
namespace = "protect.card_locker"
compileSdk {
version = release(37)
}
compileSdk = 36
defaultConfig {
applicationId = "me.hackerchick.catima"
minSdk {
version = release(23)
}
targetSdk {
version = release(37)
}
versionCode = 167
versionName = "2.43.0"
minSdk = 23
targetSdk = 36
versionCode = 1002
versionName = "2.45.0"
vectorDrawables.useSupportLibrary = true
multiDexEnabled = true
@@ -65,8 +59,14 @@ android {
dimension = "type"
isDefault = true
}
create("fdroidLegacy") {
dimension = "type"
versionNameSuffix = " (F-Droid)"
versionCode = defaultConfig.versionCode!! - 1
}
create("gplay") {
dimension = "type"
versionNameSuffix = " (Google)"
// Google doesn't allow donation links
buildConfigField("boolean", "showDonate", "false")
@@ -119,6 +119,8 @@ android {
}
dependencies {
implementation(project(":shared"))
// AndroidX
implementation(libs.androidx.appcompat.appcompat)
implementation(libs.androidx.constraintlayout.constraintlayout)
@@ -171,7 +173,6 @@ dependencies {
tasks.register("copyRawResFiles", Copy::class) {
from(
layout.projectDirectory.file("../CHANGELOG.md"),
layout.projectDirectory.file("../PRIVACY.md")
)
into(layout.projectDirectory.dir("src/main/res/raw"))
@@ -179,7 +180,7 @@ tasks.register("copyRawResFiles", Copy::class) {
}.also {
tasks.preBuild.dependsOn(it)
tasks.getByName<Delete>("clean") {
val filesNamesToDelete = listOf("CHANGELOG", "PRIVACY")
val filesNamesToDelete = listOf("PRIVACY")
filesNamesToDelete.forEach { fileName ->
delete(layout.projectDirectory.file("src/main/res/raw/${fileName.lowercase()}.md"))
}
+27
View File
@@ -13,6 +13,12 @@
<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.POST_NOTIFICATIONS" />
<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
@@ -226,5 +232,26 @@
<action android:name="android.service.controls.ControlsProviderService" />
</intent-filter>
</service>
<service
android:name=".wearos.BluetoothServerService"
android:exported="false"
android:foregroundServiceType="connectedDevice" />
<activity
android:name=".wearos.BluetoothPairingActivity"
android:exported="false"
android:excludeFromRecents="true"
android:launchMode="singleTop"
android:theme="@style/AppTheme.Transparent" />
<receiver
android:name=".wearos.BluetoothPairingReceiver"
android:exported="false">
<intent-filter>
<action android:name="protect.card_locker.wearos.action.ALLOW_DEVICE" />
<action android:name="protect.card_locker.wearos.action.BLOCK_DEVICE" />
</intent-filter>
</receiver>
</application>
</manifest>
@@ -69,16 +69,7 @@ fun AboutScreenContent(
stringResource(R.string.version_history),
content.versionHistory,
modifier = Modifier.testTag("card_version_history"),
onClickUrl = "https://catima.app/changelog/",
onClickDialogText = AnnotatedString.fromHtml(
htmlString = content.historyHtml,
linkStyles = TextLinkStyles(
style = SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colorScheme.primary
)
)
)
onClickUrl = "https://catima.app/changelog/"
)
CatimaAboutSection(
stringResource(R.string.credits),
@@ -62,18 +62,6 @@ public class AboutContent {
return contributors.replace("\n", "<br />");
}
public String getHistoryHtml() {
String versionHistory;
try {
versionHistory = Utils.readTextFile(context, R.raw.changelog)
.replace("# Changelog\n\n", "");
} catch (IOException ignored) {
return "";
}
return Utils.linkify(Utils.basicMDToHTML(versionHistory))
.replace("\n", "<br />");
}
public String getLicenseHtml() {
try {
return Utils.readTextFile(context, R.raw.license);
@@ -240,7 +240,6 @@ public class ImportURIHelper {
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
sendIntent.putExtra(Intent.EXTRA_TEXT, text.toString());
sendIntent.setType("text/plain");
@@ -14,6 +14,7 @@ import android.widget.RemoteViews
import androidx.core.widget.RemoteViewsCompat
import protect.card_locker.DBHelper.LoyaltyCardArchiveFilter
import protect.card_locker.cardview.LoyaltyCardViewActivity
import protect.card_locker.shared.ForegroundColorHelper
class ListWidget : AppWidgetProvider() {
fun updateAll(context: Context) {
@@ -100,7 +101,7 @@ class ListWidget : AppWidgetProvider() {
// Note: Android 5 will not use bitmaps
val remoteViews = RemoteViews(context.packageName, R.layout.list_widget_item).apply {
val headerColor = Utils.getHeaderColor(context, loyaltyCard)
val foreground = if (Utils.needsDarkForeground(headerColor)) Color.BLACK else Color.WHITE
val foreground = if (ForegroundColorHelper.needsDarkForeground(headerColor)) Color.BLACK else Color.WHITE
setInt(R.id.item_container_foreground, "setBackgroundColor", headerColor)
val icon = loyaltyCard.getImageThumbnail(context)
// FIXME: The icon flow causes a crash up to Android 12L, so force anything below 33 down this path
@@ -91,6 +91,7 @@ import java.util.concurrent.Callable;
import protect.card_locker.async.TaskHandler;
import protect.card_locker.databinding.LayoutChipChoiceBinding;
import protect.card_locker.databinding.LoyaltyCardEditActivityBinding;
import protect.card_locker.shared.ForegroundColorHelper;
import protect.card_locker.viewmodels.LoyaltyCardEditActivityViewModel;
public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements BarcodeImageWriterResultCallback, ColorPickerDialogListener {
@@ -896,8 +897,8 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
Integer headerColor = viewModel.getLoyaltyCard().headerColor;
if (headerColor != null) {
thumbnail.setOnClickListener(new ChooseCardImage());
thumbnailEditIcon.setBackgroundColor(Utils.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
thumbnailEditIcon.setColorFilter(Utils.needsDarkForeground(headerColor) ? Color.WHITE : Color.BLACK);
thumbnailEditIcon.setBackgroundColor(ForegroundColorHelper.Companion.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
thumbnailEditIcon.setColorFilter(ForegroundColorHelper.Companion.needsDarkForeground(headerColor) ? Color.WHITE : Color.BLACK);
}
onResuming = false;
@@ -919,18 +920,18 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
setLoyaltyCardHeaderColor(headerColor);
thumbnail.setBackgroundColor(Utils.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
thumbnail.setBackgroundColor(ForegroundColorHelper.Companion.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
thumbnailEditIcon.setBackgroundColor(Utils.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
thumbnailEditIcon.setColorFilter(Utils.needsDarkForeground(headerColor) ? Color.WHITE : Color.BLACK);
thumbnailEditIcon.setBackgroundColor(ForegroundColorHelper.Companion.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
thumbnailEditIcon.setColorFilter(ForegroundColorHelper.Companion.needsDarkForeground(headerColor) ? Color.WHITE : Color.BLACK);
} else {
generateIcon(storeFieldEdit.getText().toString().trim());
Integer headerColor = viewModel.getLoyaltyCard().headerColor;
if (headerColor != null) {
thumbnailEditIcon.setBackgroundColor(Utils.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
thumbnailEditIcon.setColorFilter(Utils.needsDarkForeground(headerColor) ? Color.WHITE : Color.BLACK);
thumbnailEditIcon.setBackgroundColor(ForegroundColorHelper.Companion.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
thumbnailEditIcon.setColorFilter(ForegroundColorHelper.Companion.needsDarkForeground(headerColor) ? Color.WHITE : Color.BLACK);
}
}
}
@@ -1,9 +1,9 @@
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;
@@ -11,6 +11,7 @@ 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 {
@@ -41,5 +42,10 @@ public class LoyaltyCardLockerApplication extends Application {
// 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);
}
}
@@ -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
@@ -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 {
@@ -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) {
@@ -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) {
@@ -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(
@@ -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()
}
})
}
@@ -556,7 +553,7 @@ class MainActivity : CatimaAppCompatActivity(), CardAdapterListener {
return
}
processParseResultList(parseResultList, null, true)
processParseResultList(parseResultList)
}
private fun extractIntentFields(intent: Intent) {
@@ -0,0 +1,11 @@
package protect.card_locker
object NotificationInfo {
object WearBluetooth {
const val NOTIFICATION_ID = 1001
const val CHANNEL_ID = "catima_wear_bt"
const val PAIRING_CHANNEL_ID = "catima_wear_bt_pairing"
const val CRITICAL_ERROR_NOTIFICATION_ID = 1002
const val CRITICAL_ERROR_CHANNEL_ID = "catima_wear_bt_critical_error"
}
}
@@ -18,6 +18,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.LinkedList;
import protect.card_locker.cardview.LoyaltyCardViewActivity;
import protect.card_locker.shared.ForegroundColorHelper;
public class ShortcutHelper {
/**
@@ -102,7 +103,7 @@ public class ShortcutHelper {
if (iconBitmap == null) {
iconBitmap = Utils.generateIcon(context, loyaltyCard, true).getLetterTile();
} else {
iconBitmap = createAdaptiveBitmap(iconBitmap, Utils.needsDarkForeground(Utils.getHeaderColor(context, loyaltyCard)) ? Color.BLACK : Color.WHITE);
iconBitmap = createAdaptiveBitmap(iconBitmap, ForegroundColorHelper.Companion.needsDarkForeground(Utils.getHeaderColor(context, loyaltyCard)) ? Color.BLACK : Color.WHITE);
}
IconCompat icon = IconCompat.createWithAdaptiveBitmap(iconBitmap);
@@ -39,7 +39,6 @@ import androidx.annotation.Nullable;
import androidx.annotation.RawRes;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.graphics.ColorUtils;
import androidx.core.graphics.Insets;
import androidx.core.os.LocaleListCompat;
import androidx.core.view.ViewCompat;
@@ -95,6 +94,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import protect.card_locker.preferences.Settings;
import protect.card_locker.shared.ForegroundColorHelper;
public class Utils {
private static final String TAG = "Catima";
@@ -115,8 +115,6 @@ public class Utils {
public static final String CARD_IMAGE_FILENAME_REGEX = "^(card_)(\\d+)(_(?:front|back|icon)\\.png)$";
static final double LUMINANCE_MIDPOINT = 0.5;
static final int BITMAP_SIZE_SMALL = 512;
static final int BITMAP_SIZE_BIG = 1600;
@@ -147,11 +145,7 @@ public class Utils {
}
return new LetterBitmap(context, store, store,
tileLetterFontSize, pixelSize, pixelSize, backgroundColor, needsDarkForeground(backgroundColor) ? Color.BLACK : Color.WHITE);
}
static public boolean needsDarkForeground(Integer backgroundColor) {
return ColorUtils.calculateLuminance(backgroundColor) > LUMINANCE_MIDPOINT;
tileLetterFontSize, pixelSize, pixelSize, backgroundColor, ForegroundColorHelper.Companion.needsDarkForeground(backgroundColor) ? Color.BLACK : Color.WHITE);
}
static public List<ParseResult> retrieveBarcodesFromImage(Context context, Uri uri) {
@@ -1070,7 +1064,7 @@ public class Utils {
if (icon != null) {
// Use header colour to decide if this image will need a white or black background
backgroundOrIcon.setBackgroundColor(needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
backgroundOrIcon.setBackgroundColor(ForegroundColorHelper.Companion.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
// Ensure correct cropping style
backgroundOrIcon.setScaleType(Utils.getRecommendedScaleTypeForThumbnailImage(icon));
@@ -1100,7 +1094,7 @@ public class Utils {
// Actually set the text and colour
textWhenNoImage.setVisibility(View.VISIBLE);
textWhenNoImage.setText(loyaltyCard.store);
textWhenNoImage.setTextColor(Utils.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
textWhenNoImage.setTextColor(ForegroundColorHelper.Companion.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
}
return headerColor;
@@ -2,6 +2,7 @@ package protect.card_locker.cardimageview
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
import android.graphics.Bitmap
import android.os.Bundle
import android.view.WindowManager
@@ -17,8 +18,15 @@ import androidx.compose.foundation.gestures.transformable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
@@ -32,9 +40,11 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntSize
import androidx.core.content.FileProvider
import protect.card_locker.CatimaComponentActivity
import protect.card_locker.ImageLocationType
import protect.card_locker.R
@@ -68,6 +78,8 @@ class LoyaltyCardImageViewActivity : CatimaComponentActivity() {
CatimaTheme {
LoyaltyCardImageViewScreen(
bitmap = bitmap,
imageLocationType = imageLocationType,
cardId = loyaltyCardId,
contentDescriptionRes = imageLocationType.descriptionRes(),
onBackPressedDispatcher = onBackPressedDispatcher
)
@@ -124,9 +136,12 @@ class LoyaltyCardImageViewActivity : CatimaComponentActivity() {
@Composable
fun LoyaltyCardImageViewScreen(
bitmap: Bitmap,
imageLocationType: ImageLocationType,
cardId: Int,
@StringRes contentDescriptionRes: Int,
onBackPressedDispatcher: OnBackPressedDispatcher,
) {
val context = LocalContext.current
var scale by remember { mutableFloatStateOf(1F) }
var offset by remember { mutableStateOf(Offset.Zero) }
var size by remember { mutableStateOf(IntSize.Zero) }
@@ -144,7 +159,24 @@ fun LoyaltyCardImageViewScreen(
topBar = {
CatimaTopAppBar(
title = stringResource(contentDescriptionRes),
onBackPressedDispatcher = onBackPressedDispatcher
onBackPressedDispatcher = onBackPressedDispatcher,
actions = {
var expanded by remember { mutableStateOf(false) }
IconButton(onClick = { expanded = !expanded }) {
Icon(
imageVector = Icons.Filled.MoreVert,
contentDescription = stringResource(R.string.action_more_options)
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(onClick = { openInImageGallery(context, cardId, imageLocationType) },
text = { Text(stringResource(R.string.open_in_gallery)) },
)
}
}
)
}
) { innerPadding ->
@@ -196,4 +228,13 @@ private fun Offset.clampTo(size: IntSize, scale: Float): Offset {
)
}
private const val MAX_IMAGE_SCALE = 5F
private fun openInImageGallery(context: Context, cardId: Int, imageLocationType: ImageLocationType){
val imagePath = Utils.getCardImageFileName(cardId, imageLocationType)
val viewInGalleryIntent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(FileProvider.getUriForFile(context, context.packageName, context.getFileStreamPath(imagePath)), "image/*")
flags = FLAG_GRANT_READ_URI_PERMISSION
}
context.startActivity(viewInGalleryIntent)
}
private const val MAX_IMAGE_SCALE = 5F
@@ -54,6 +54,7 @@ import protect.card_locker.cardimageview.LoyaltyCardImageViewActivity;
import protect.card_locker.databinding.LoyaltyCardViewLayoutBinding;
import protect.card_locker.preferences.Settings;
import protect.card_locker.preferences.SettingsActivity;
import protect.card_locker.shared.ForegroundColorHelper;
public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements BarcodeImageWriterResultCallback {
private LoyaltyCardViewLayoutBinding binding;
@@ -598,20 +599,20 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
// Set bottomAppBar and system navigation bar color
binding.bottomAppBar.setBackgroundColor(darkenedColor);
Utils.setNavigationBarColor(null, window, darkenedColor, Utils.needsDarkForeground(darkenedColor));
Utils.setNavigationBarColor(null, window, darkenedColor, ForegroundColorHelper.Companion.needsDarkForeground(darkenedColor));
int complementaryColor = Utils.getComplementaryColor(darkenedColor);
binding.fabEdit.setBackgroundTintList(ColorStateList.valueOf(complementaryColor));
Drawable editButtonIcon = binding.fabEdit.getDrawable();
editButtonIcon.mutate();
editButtonIcon.setTint(Utils.needsDarkForeground(complementaryColor) ? Color.BLACK : Color.WHITE);
editButtonIcon.setTint(ForegroundColorHelper.Companion.needsDarkForeground(complementaryColor) ? Color.BLACK : Color.WHITE);
binding.fabEdit.setImageDrawable(editButtonIcon);
Bitmap icon = loyaltyCard.getImageThumbnail(this);
Utils.setIconOrTextWithBackground(this, loyaltyCard, icon, binding.iconImage, binding.iconText, 1);
// If the background is very bright, we should use dark icons
backgroundNeedsDarkIcons = Utils.needsDarkForeground(backgroundHeaderColor);
backgroundNeedsDarkIcons = ForegroundColorHelper.Companion.needsDarkForeground(backgroundHeaderColor);
fixBottomAppBarImageButtonColor(binding.bottomAppBarInfoButton);
fixBottomAppBarImageButtonColor(binding.bottomAppBarPreviousButton);
@@ -3,6 +3,7 @@ package protect.card_locker.compose
import androidx.activity.OnBackPressedDispatcher
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -22,7 +23,9 @@ import protect.card_locker.preferences.Settings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CatimaTopAppBar(title: String, onBackPressedDispatcher: OnBackPressedDispatcher?) {
fun CatimaTopAppBar(title: String,
onBackPressedDispatcher: OnBackPressedDispatcher?,
actions : @Composable RowScope.() -> Unit = {}) {
// Use pure black in OLED theme
val context = LocalContext.current
val settings = Settings(context)
@@ -51,6 +54,7 @@ fun CatimaTopAppBar(title: String, onBackPressedDispatcher: OnBackPressedDispatc
)
}
}
}
},
actions = actions
)
}
@@ -112,4 +112,8 @@ public class Settings {
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);
}
}
@@ -1,22 +1,42 @@
package protect.card_locker.preferences
import android.app.Activity
import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.view.LayoutInflater
import android.view.View
import android.view.MenuItem
import android.widget.LinearLayout
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
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.dialog.MaterialAlertDialogBuilder
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.shared.BluetoothPermissionHelper
import protect.card_locker.shared.WearBluetoothSecurity
import protect.card_locker.wearos.BluetoothPairingNotificationManager
import protect.card_locker.wearos.WearSyncPermissionRequester
class SettingsActivity : CatimaAppCompatActivity() {
@@ -81,11 +101,62 @@ class SettingsActivity : CatimaAppCompatActivity() {
class SettingsFragment : PreferenceFragmentCompat() {
var mReloadMain: Boolean = false
private lateinit var wearSyncPermissionRequester: WearSyncPermissionRequester
private var currentDevicesDialog: AlertDialog? = null
private var currentDevicesDialogIsBlocked: Boolean = false
private val devicesChangedReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != BluetoothPairingNotificationManager.ACTION_DEVICES_CHANGED) {
return
}
findPreference<Preference>(getString(R.string.settings_key_wear_sync_devices))?.let { updateWearSyncAllowedDevicesSummary(it) }
findPreference<Preference>(getString(R.string.settings_key_wear_sync_blocked_devices))?.let { updateWearSyncBlockedDevicesSummary(it) }
if (currentDevicesDialog?.isShowing != true) {
return
}
val isBlocked = currentDevicesDialogIsBlocked
currentDevicesDialog?.dismiss()
if (isBlocked) {
showWearSyncBlockedDevicesDialog()
} else {
showWearSyncAllowedDevicesDialog()
}
}
}
override fun onResume() {
super.onResume()
wearSyncPermissionRequester.synchronize()
findPreference<Preference>(getString(R.string.settings_key_wear_sync_devices))?.let { updateWearSyncAllowedDevicesSummary(it) }
findPreference<Preference>(getString(R.string.settings_key_wear_sync_blocked_devices))?.let { updateWearSyncBlockedDevicesSummary(it) }
ContextCompat.registerReceiver(
requireContext(),
devicesChangedReceiver,
IntentFilter(BluetoothPairingNotificationManager.ACTION_DEVICES_CHANGED),
ContextCompat.RECEIVER_NOT_EXPORTED
)
}
override fun onPause() {
super.onPause()
requireContext().unregisterReceiver(devicesChangedReceiver)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
wearSyncPermissionRequester = WearSyncPermissionRequester(this, requireContext())
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preferences)
// Show pretty names and summaries
setupThemePreference()
setupOledDarkPreference()
setupLocalePreference()
setupCrashReporterPreference()
setupWearSyncPreference()
setupWearSyncDevicePreferences()
}
private fun setupThemePreference() {
val themePreference = findPreference<ListPreference>(getString(R.string.settings_key_theme))
themePreference!!.setOnPreferenceChangeListener { _, o ->
when (o.toString()) {
@@ -101,13 +172,17 @@ class SettingsActivity : CatimaAppCompatActivity() {
}
true
}
}
private fun setupOledDarkPreference() {
val oledDarkPreference = findPreference<Preference>(getString(R.string.settings_key_oled_dark))
oledDarkPreference!!.setOnPreferenceChangeListener { _, _ ->
refreshActivity(true)
refreshActivity()
true
}
}
private fun setupLocalePreference() {
val localePreference =
findPreference<ListPreference>(getString(R.string.settings_key_locale))!!
localePreference.let {
@@ -148,7 +223,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
@@ -156,14 +231,205 @@ class SettingsActivity : CatimaAppCompatActivity() {
AppCompatDelegate.setApplicationLocales(if (newLocale.isEmpty()) LocaleListCompat.getEmptyLocaleList() else LocaleListCompat.create(Utils.stringToLocale(newLocale)))
true
}
}
private fun setupCrashReporterPreference() {
// 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 setupWearSyncPreference() {
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
}
}
}
private fun setupWearSyncDevicePreferences() {
val allowedPreference = findPreference<Preference>(getString(R.string.settings_key_wear_sync_devices))
allowedPreference!!.setOnPreferenceClickListener {
showWearSyncAllowedDevicesDialog()
true
}
updateWearSyncAllowedDevicesSummary(allowedPreference)
val blockedPreference = findPreference<Preference>(getString(R.string.settings_key_wear_sync_blocked_devices))
blockedPreference!!.setOnPreferenceClickListener {
showWearSyncBlockedDevicesDialog()
true
}
updateWearSyncBlockedDevicesSummary(blockedPreference)
}
private fun updateWearSyncAllowedDevicesSummary(preference: Preference) {
val count = WearBluetoothSecurity.listTrustedDevices(requireContext()).size
preference.summary = if (count == 0) {
getString(R.string.settings_wear_sync_no_devices)
} else {
resources.getQuantityString(R.plurals.settings_wear_sync_devices_summary_count, count, count)
}
}
private fun updateWearSyncBlockedDevicesSummary(preference: Preference) {
val count = WearBluetoothSecurity.listBlockedDevices(requireContext()).size
preference.summary = if (count == 0) {
getString(R.string.settings_wear_sync_no_blocked_devices)
} else {
resources.getQuantityString(R.plurals.settings_wear_sync_blocked_devices_summary_count, count, count)
}
}
@SuppressLint("MissingPermission")
private fun showWearSyncDeviceListDialog(
titleRes: Int,
noDevicesRes: Int,
messageRes: Int,
addresses: Set<String>,
isBlocked: Boolean,
onRemove: (String, String) -> Unit
) {
val context = requireContext()
if (!BluetoothPermissionHelper.isBluetoothConnectGranted(context)) {
showWearSyncPermissionDeniedSnackbar()
return
}
val knownAddresses = addresses.toList()
currentDevicesDialog?.dismiss()
currentDevicesDialogIsBlocked = isBlocked
if (knownAddresses.isEmpty()) {
currentDevicesDialog = MaterialAlertDialogBuilder(context)
.setTitle(titleRes)
.setMessage(noDevicesRes)
.setOnDismissListener {
currentDevicesDialog = null
currentDevicesDialogIsBlocked = false
}
.setPositiveButton(android.R.string.ok) { dialog, _ -> dialog.dismiss() }
.show()
return
}
val adapter = context.getSystemService(BLUETOOTH_SERVICE) as? BluetoothManager
val bondedDevices = try {
adapter?.adapter?.bondedDevices ?: emptySet()
} catch (_: SecurityException) {
emptySet<BluetoothDevice>()
}
val deviceMap = bondedDevices.associateBy { it.address }
val content = View.inflate(context, R.layout.wear_sync_device_list, null)
content.findViewById<TextView>(R.id.wear_sync_device_list_message).text = getString(messageRes)
val container = content.findViewById<LinearLayout>(R.id.wear_sync_device_list_container)
knownAddresses.forEach { address ->
val name = deviceName(deviceMap[address], address)
val itemView = LayoutInflater.from(context).inflate(R.layout.wear_sync_device_list_item, container, false) as TextView
itemView.text = getString(R.string.settings_wear_sync_device_name, name, address)
itemView.setOnClickListener { onRemove(address, name) }
container.addView(itemView)
}
currentDevicesDialog = MaterialAlertDialogBuilder(context)
.setTitle(titleRes)
.setView(content)
.setOnDismissListener {
currentDevicesDialog = null
currentDevicesDialogIsBlocked = false
}
.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.dismiss() }
.show()
}
private fun showWearSyncAllowedDevicesDialog() {
showWearSyncDeviceListDialog(
R.string.settings_wear_sync_devices,
R.string.settings_wear_sync_no_devices,
R.string.settings_wear_sync_tap_to_remove,
WearBluetoothSecurity.listTrustedDevices(requireContext()),
isBlocked = false
) { address, name -> confirmRemoveAllowedDevice(address, name) }
}
private fun showWearSyncBlockedDevicesDialog() {
showWearSyncDeviceListDialog(
R.string.settings_wear_sync_blocked_devices,
R.string.settings_wear_sync_no_blocked_devices,
R.string.settings_wear_sync_tap_to_remove,
WearBluetoothSecurity.listBlockedDevices(requireContext()),
isBlocked = true
) { address, name -> confirmRemoveBlockedDevice(address, name) }
}
private fun confirmRemoveAllowedDevice(address: String, name: String) {
confirmRemoveDevice(
address,
name,
R.string.settings_wear_sync_unpair_message,
onRemove = { WearBluetoothSecurity.untrustDevice(requireContext(), address) }
)
}
private fun confirmRemoveBlockedDevice(address: String, name: String) {
confirmRemoveDevice(
address,
name,
R.string.settings_wear_sync_unblock_message,
onRemove = { WearBluetoothSecurity.unblockDevice(requireContext(), address) }
)
}
private fun confirmRemoveDevice(address: String, name: String, messageRes: Int, onRemove: () -> Unit) {
val context = requireContext()
MaterialAlertDialogBuilder(context)
.setTitle(R.string.settings_wear_sync_unpair_title)
.setMessage(getString(messageRes, name))
.setPositiveButton(R.string.settings_wear_sync_unpair) { dialog, _ ->
onRemove()
BluetoothPairingNotificationManager.cancel(context, address)
BluetoothPairingNotificationManager.notifyDevicesChanged(context)
findPreference<Preference>(getString(R.string.settings_key_wear_sync_devices))?.let { updateWearSyncAllowedDevicesSummary(it) }
findPreference<Preference>(getString(R.string.settings_key_wear_sync_blocked_devices))?.let { updateWearSyncBlockedDevicesSummary(it) }
dialog.dismiss()
}
.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.dismiss() }
.show()
}
private fun deviceName(device: BluetoothDevice?, fallback: String): String {
return try { device?.name } catch (_: SecurityException) { null } ?: fallback
}
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()
}
}
@@ -0,0 +1,68 @@
package protect.card_locker.wearos
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import protect.card_locker.R
import protect.card_locker.shared.WearBluetoothSecurity
class BluetoothPairingActivity : AppCompatActivity() {
private var currentDialog: AlertDialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
showPairingDialog(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
showPairingDialog(intent)
}
override fun onDestroy() {
currentDialog?.dismiss()
currentDialog = null
super.onDestroy()
}
private fun showPairingDialog(intent: Intent) {
val address = intent.getStringExtra(BluetoothPairingConstants.DEVICE_ADDRESS) ?: return finish()
val deviceName = intent.getStringExtra(BluetoothPairingConstants.DEVICE_NAME) ?: address
val token = intent.getStringExtra(BluetoothPairingConstants.DEVICE_TOKEN)
val isMismatch = intent.getBooleanExtra(BluetoothPairingConstants.IS_MISMATCH, false)
val titleRes = if (isMismatch) {
R.string.wear_bt_pairing_mismatch_title
} else {
R.string.wear_bt_pairing_title
}
val messageRes = if (isMismatch) {
R.string.wear_bt_pairing_mismatch_message
} else {
R.string.wear_bt_pairing_message
}
currentDialog?.dismiss()
currentDialog = MaterialAlertDialogBuilder(this)
.setTitle(getString(titleRes, deviceName))
.setMessage(getString(messageRes, deviceName))
.setPositiveButton(R.string.wear_bt_pairing_allow) { _, _ ->
WearBluetoothSecurity.trustDevice(this, address, token)
BluetoothPairingNotificationManager.updateResultNotification(this, address, deviceName, true)
BluetoothPairingNotificationManager.notifyDevicesChanged(this)
finish()
}
.setNegativeButton(R.string.wear_bt_pairing_block) { _, _ ->
WearBluetoothSecurity.blockDevice(this, address)
BluetoothPairingNotificationManager.updateResultNotification(this, address, deviceName, false)
BluetoothPairingNotificationManager.notifyDevicesChanged(this)
finish()
}
.setOnCancelListener { finish() }
.setCancelable(true)
.show()
}
}
@@ -0,0 +1,8 @@
package protect.card_locker.wearos
object BluetoothPairingConstants {
const val DEVICE_ADDRESS = "device_address"
const val DEVICE_NAME = "device_name"
const val DEVICE_TOKEN = "device_token"
const val IS_MISMATCH = "is_mismatch"
}
@@ -0,0 +1,122 @@
package protect.card_locker.wearos
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.widget.Toast
import androidx.core.app.NotificationCompat
import protect.card_locker.NotificationInfo
import protect.card_locker.R
object BluetoothPairingNotificationManager {
const val ACTION_DEVICES_CHANGED = "protect.card_locker.wearos.ACTION_DEVICES_CHANGED"
fun notifyDevicesChanged(context: Context) {
context.sendBroadcast(Intent(ACTION_DEVICES_CHANGED).apply { setPackage(context.packageName) })
}
fun showAuthorizationNotification(
context: Context,
deviceName: String,
address: String,
token: String,
isMismatch: Boolean = false
) {
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
NotificationInfo.WearBluetooth.PAIRING_CHANNEL_ID,
context.getString(R.string.wear_bt_pairing_channel_name),
NotificationManager.IMPORTANCE_HIGH
)
notificationManager.createNotificationChannel(channel)
}
val titleRes = if (isMismatch) {
R.string.wear_bt_pairing_mismatch_title
} else {
R.string.wear_bt_pairing_title
}
val messageRes = if (isMismatch) {
R.string.wear_bt_pairing_mismatch_message
} else {
R.string.wear_bt_pairing_message
}
val allowIntent = Intent(context, BluetoothPairingReceiver::class.java).apply {
action = BluetoothPairingReceiver.ACTION_ALLOW
putExtra(BluetoothPairingConstants.DEVICE_ADDRESS, address)
putExtra(BluetoothPairingConstants.DEVICE_NAME, deviceName)
putExtra(BluetoothPairingConstants.DEVICE_TOKEN, token)
}
val blockIntent = Intent(context, BluetoothPairingReceiver::class.java).apply {
action = BluetoothPairingReceiver.ACTION_BLOCK
putExtra(BluetoothPairingConstants.DEVICE_ADDRESS, address)
putExtra(BluetoothPairingConstants.DEVICE_NAME, deviceName)
putExtra(BluetoothPairingConstants.IS_MISMATCH, isMismatch)
}
val allowPendingIntent = PendingIntent.getBroadcast(
context,
address.hashCode(),
allowIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val blockPendingIntent = PendingIntent.getBroadcast(
context,
address.hashCode() + 1,
blockIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val contentIntent = PendingIntent.getActivity(
context,
address.hashCode() + 2,
Intent(context, BluetoothPairingActivity::class.java).apply {
putExtra(BluetoothPairingConstants.DEVICE_ADDRESS, address)
putExtra(BluetoothPairingConstants.DEVICE_NAME, deviceName)
putExtra(BluetoothPairingConstants.DEVICE_TOKEN, token)
putExtra(BluetoothPairingConstants.IS_MISMATCH, isMismatch)
},
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(context, NotificationInfo.WearBluetooth.PAIRING_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification_catima)
.setContentTitle(context.getString(titleRes, deviceName))
.setContentText(context.getString(messageRes, deviceName))
.setStyle(NotificationCompat.BigTextStyle().bigText(context.getString(messageRes, deviceName)))
.addAction(0, context.getString(R.string.wear_bt_pairing_allow), allowPendingIntent)
.addAction(0, context.getString(R.string.wear_bt_pairing_block), blockPendingIntent)
.setContentIntent(contentIntent)
.setAutoCancel(false)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_STATUS)
.build()
notificationManager.notify(notificationId(address), notification)
}
fun cancel(context: Context, address: String) {
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.cancel(notificationId(address))
}
fun updateResultNotification(context: Context, address: String, deviceName: String, allowed: Boolean) {
cancel(context, address)
val message = if (allowed) {
context.getString(R.string.wear_bt_pairing_allowed, deviceName)
} else {
context.getString(R.string.wear_bt_pairing_blocked, deviceName)
}
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
fun notificationId(address: String): Int = address.hashCode()
}
@@ -0,0 +1,33 @@
package protect.card_locker.wearos
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import protect.card_locker.shared.WearBluetoothSecurity
class BluetoothPairingReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val address = intent.getStringExtra(BluetoothPairingConstants.DEVICE_ADDRESS) ?: return
val deviceName = intent.getStringExtra(BluetoothPairingConstants.DEVICE_NAME) ?: address
val token = intent.getStringExtra(BluetoothPairingConstants.DEVICE_TOKEN)
when (intent.action) {
ACTION_ALLOW -> {
WearBluetoothSecurity.trustDevice(context, address, token)
BluetoothPairingNotificationManager.updateResultNotification(context, address, deviceName, true)
BluetoothPairingNotificationManager.notifyDevicesChanged(context)
}
ACTION_BLOCK -> {
WearBluetoothSecurity.blockDevice(context, address)
BluetoothPairingNotificationManager.updateResultNotification(context, address, deviceName, false)
BluetoothPairingNotificationManager.notifyDevicesChanged(context)
}
}
}
companion object {
const val ACTION_ALLOW = "protect.card_locker.wearos.action.ALLOW_DEVICE"
const val ACTION_BLOCK = "protect.card_locker.wearos.action.BLOCK_DEVICE"
}
}
@@ -0,0 +1,310 @@
package protect.card_locker.wearos
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothServerSocket
import android.bluetooth.BluetoothSocket
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import org.json.JSONArray
import org.json.JSONObject
import protect.card_locker.DBHelper
import protect.card_locker.NotificationInfo
import protect.card_locker.R
import protect.card_locker.Utils
import protect.card_locker.shared.BluetoothPermissionHelper
import protect.card_locker.shared.WearBluetoothProtocol
import protect.card_locker.shared.WearBluetoothSecurity
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.io.PrintWriter
class BluetoothServerService : Service() {
companion object {
private const val TAG = "CatimaBtServer"
private const val NOTIFICATION_ID = NotificationInfo.WearBluetooth.NOTIFICATION_ID
private const val CHANNEL_ID = NotificationInfo.WearBluetooth.CHANNEL_ID
}
private var serverThread: AcceptThread? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (!BluetoothPermissionHelper.isBluetoothConnectGranted(this)) {
Log.w(TAG, "BLUETOOTH_CONNECT permission not granted, stopping")
stopSelf()
return START_NOT_STICKY
}
val adapter = (getSystemService(BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter
if (adapter == null || !adapter.isEnabled) {
Log.w(TAG, "Bluetooth not available or disabled")
stopSelf()
return START_NOT_STICKY
}
// Avoid tearing down a working accept socket every time Settings is resumed.
if (serverThread?.isAlive == true) {
Log.d(TAG, "Bluetooth server already listening")
return START_STICKY
}
startForegroundWithNotification()
serverThread?.cancel()
serverThread = AcceptThread(adapter).also { it.start() }
Log.d(TAG, "Bluetooth server started")
return START_STICKY
}
private fun startForegroundWithNotification() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
getString(R.string.wear_bt_channel_name),
NotificationManager.IMPORTANCE_MIN
).apply { setShowBadge(false) }
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
}
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(getString(R.string.wear_bt_notification_title))
.setSmallIcon(R.drawable.ic_notification_sync)
.setPriority(NotificationCompat.PRIORITY_MIN)
.setSilent(true)
.build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(NOTIFICATION_ID, notification,
android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE)
} else {
startForeground(NOTIFICATION_ID, notification)
}
}
override fun onDestroy() {
serverThread?.cancel()
serverThread = null
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder? = null
private inner class AcceptThread(adapter: BluetoothAdapter) : Thread() {
private var serverSocket: BluetoothServerSocket? = null
@Volatile private var running = true
init {
try {
if (BluetoothPermissionHelper.isBluetoothConnectGranted(this@BluetoothServerService)) {
serverSocket = adapter.listenUsingRfcommWithServiceRecord(
WearBluetoothProtocol.BT_SERVICE_NAME,
WearBluetoothProtocol.BT_SERVICE_UUID
)
} else {
Log.w(TAG, "BLUETOOTH_CONNECT permission missing, cannot open server socket")
running = false
}
} catch (e: SecurityException) {
Log.e(TAG, "BLUETOOTH_CONNECT permission missing, cannot create server socket", e)
running = false
} catch (e: Exception) {
Log.e(TAG, "Failed to create server socket", e)
running = false
}
}
override fun run() {
Log.d(TAG, "Listening for Bluetooth connections")
while (running) {
val socket: BluetoothSocket = try {
serverSocket?.accept() ?: break
} catch (e: Exception) {
if (running) Log.e(TAG, "Accept failed", e)
break
}
// Handle each client on its own thread so a slow/stuck client
// cannot block the accept loop.
Thread { handleConnection(socket) }.start()
}
Log.d(TAG, "Accept loop ended")
if (running) {
Log.w(TAG, "Accept loop exited unexpectedly, restarting service")
stopSelf()
}
}
private fun handleConnection(socket: BluetoothSocket) {
val address = try { socket.remoteDevice.address } catch (_: SecurityException) { null } ?: return
val deviceName = try { socket.remoteDevice.name } catch (_: SecurityException) { address }
Log.d(TAG, "Connected to $deviceName ($address)")
try {
val reader = BufferedReader(InputStreamReader(socket.inputStream, "UTF-8"))
val writer = PrintWriter(OutputStreamWriter(socket.outputStream, "UTF-8"), false)
val firstLine = reader.readLine()?.trim() ?: return
if (firstLine == WearBluetoothProtocol.BT_CMD_VERSIONS) {
val versions = JSONArray().put("${WearBluetoothProtocol.PROTOCOL_VERSION}.${WearBluetoothProtocol.PROTOCOL_MINOR_VERSION}").toString()
writer.println(versions)
writer.flush()
Log.d(TAG, "Sent supported versions to $deviceName: $versions")
return
}
var commandLine: String? = firstLine
while (commandLine != null) {
val command = commandLine
Log.d(TAG, "Received command: $command from $deviceName")
if (!command.startsWith(WearBluetoothProtocol.BT_CMD_VERSION_PREFIX)) {
notAuthorized(writer)
Log.w(TAG, "Unsupported command from $deviceName: $command")
break
}
val tokenLine = reader.readLine()?.trim()
if (tokenLine == null || !tokenLine.startsWith(WearBluetoothProtocol.BT_CMD_TOKEN_PREFIX)) {
notAuthorized(writer)
Log.w(TAG, "Malformed token line from $deviceName: $tokenLine")
break
}
val token = tokenLine.removePrefix(WearBluetoothProtocol.BT_CMD_TOKEN_PREFIX)
val isTrusted = WearBluetoothSecurity.isDeviceTrusted(this@BluetoothServerService, address)
val storedToken = WearBluetoothSecurity.getDeviceToken(this@BluetoothServerService, address)
val tokenMatches = storedToken != null && storedToken == token
when {
WearBluetoothSecurity.isDeviceBlocked(this@BluetoothServerService, address) -> {
notAuthorized(writer)
Log.w(TAG, "Rejected blocked device $deviceName")
break
}
isTrusted && tokenMatches -> {
if (!serveCommand(writer, deviceName, command)) {
break
}
}
isTrusted && !tokenMatches -> {
BluetoothPairingNotificationManager.showAuthorizationNotification(
this@BluetoothServerService,
deviceName,
address,
token,
isMismatch = true
)
notAuthorized(writer)
Log.w(TAG, "Token mismatch for $deviceName")
break
}
else -> {
BluetoothPairingNotificationManager.showAuthorizationNotification(
this@BluetoothServerService,
deviceName,
address,
token,
isMismatch = false
)
notAuthorized(writer)
Log.d(TAG, "Requested authorization for $deviceName")
break
}
}
commandLine = reader.readLine()?.trim()
}
} catch (_: IOException) {
// Client closed the socket; this is a normal end-of-stream after
// the watch has received the pages it needs.
Log.d(TAG, "Connection closed by $deviceName")
} catch (e: Exception) {
Log.e(TAG, "Error handling connection from $deviceName", e)
} finally {
try { socket.close() } catch (_: Exception) { }
}
}
private fun notAuthorized(writer: PrintWriter) {
writer.println(WearBluetoothProtocol.BT_RESPONSE_NOT_AUTHORIZED)
writer.flush()
}
private fun serveCommand(
writer: PrintWriter,
deviceName: String,
command: String
): Boolean {
when {
command.startsWith(WearBluetoothProtocol.BT_CMD_CARDS_PAGE_PREFIX) -> {
val pageIndex = command.removePrefix(WearBluetoothProtocol.BT_CMD_CARDS_PAGE_PREFIX).toIntOrNull()
if (pageIndex == null || pageIndex < 0) {
Log.w(TAG, "Invalid page index from $deviceName: $command")
notAuthorized(writer)
return false
}
val json = buildCardsPageJson(pageIndex)
writer.println(json)
writer.flush()
Log.d(TAG, "Sent page $pageIndex to $deviceName")
return true
}
else -> {
Log.w(TAG, "Unsupported command from $deviceName: $command")
notAuthorized(writer)
return false
}
}
}
private fun buildCardsPageJson(pageIndex: Int): String {
val dbHelper = DBHelper(this@BluetoothServerService)
val db = dbHelper.readableDatabase
val order = Utils.getLoyaltyCardOrder(this@BluetoothServerService)
val orderDirection = Utils.getLoyaltyCardOrderDirection(this@BluetoothServerService)
val cursor = DBHelper.getLoyaltyCardCursor(
db, "", null, order, orderDirection,
DBHelper.LoyaltyCardArchiveFilter.Unarchived
)
val totalCards = cursor.count
val totalPages = if (totalCards == 0) 1 else (totalCards + WearBluetoothProtocol.PAGE_SIZE - 1) / WearBluetoothProtocol.PAGE_SIZE
val offset = pageIndex * WearBluetoothProtocol.PAGE_SIZE
val array = JSONArray()
cursor.use {
if (it.move(offset + 1)) {
val idIdx = it.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ID)
val storeIdx = it.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.STORE)
val cardIdIdx = it.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.CARD_ID)
val barcodeIdIdx = it.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BARCODE_ID)
val barcodeTypeIdx = it.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BARCODE_TYPE)
val headerColorIdx = it.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.HEADER_COLOR)
var count = 0
do {
array.put(JSONObject().apply {
put("id", it.getInt(idIdx))
put("store", it.getString(storeIdx) ?: "")
put("cardId", it.getString(cardIdIdx) ?: "")
put("barcodeId", if (it.isNull(barcodeIdIdx)) JSONObject.NULL else it.getString(barcodeIdIdx))
put("barcodeType", if (it.isNull(barcodeTypeIdx)) JSONObject.NULL else it.getString(barcodeTypeIdx))
put("headerColor", if (it.isNull(headerColorIdx)) JSONObject.NULL else it.getInt(headerColorIdx))
})
count++
} while (count < WearBluetoothProtocol.PAGE_SIZE && it.moveToNext())
}
}
return JSONObject().apply {
put("page", pageIndex)
put("totalPages", totalPages)
put("cards", array)
}.toString()
}
fun cancel() {
running = false
try { serverSocket?.close() } catch (_: Exception) { }
}
}
}
@@ -0,0 +1,167 @@
package protect.card_locker.wearos
import android.Manifest
import android.app.ForegroundServiceStartNotAllowedException
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.activity.result.ActivityResultCaller
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import protect.card_locker.NotificationInfo
import protect.card_locker.R
import protect.card_locker.preferences.Settings
import protect.card_locker.shared.BluetoothPermissionHelper
class WearSyncPermissionRequester(
caller: ActivityResultCaller,
context: Context
) {
private val context = context.applicationContext
private var pendingResultCallback: ((Boolean) -> Unit)? = null
private val permissionLauncher = caller.registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { results ->
val allGranted = results.all { it.value }
pendingResultCallback?.invoke(allGranted)
WearSyncServiceManager.onPermissionResult(this.context, allGranted)
pendingResultCallback = null
}
fun synchronize() {
WearSyncServiceManager.synchronize(context) {
permissionLauncher.launch(getRequiredPermissions())
}
}
fun onWearSyncChanged(enabled: Boolean, onPermissionResult: ((Boolean) -> Unit)? = null) {
val applied = WearSyncServiceManager.onWearSyncChanged(context, enabled) {
pendingResultCallback = onPermissionResult
permissionLauncher.launch(getRequiredPermissions())
}
if (applied) {
onPermissionResult?.invoke(true)
}
}
private fun getRequiredPermissions(): Array<String> = buildList {
if (BluetoothPermissionHelper.isBluetoothConnectRequired()) {
add(Manifest.permission.BLUETOOTH_CONNECT)
}
if (BluetoothPermissionHelper.isPostNotificationsRequired()) {
add(Manifest.permission.POST_NOTIFICATIONS)
}
}.toTypedArray()
}
object WearSyncServiceManager {
private const val TAG = "CatimaWearSyncServiceManager"
fun synchronize(context: Context, requestPermission: (() -> Unit)? = null) {
if (!Settings(context).wearSyncEnabled) {
stop(context)
} else if (arePermissionsGranted(context)) {
start(context)
} else if (requestPermission != null) {
requestPermission()
} else {
stop(context)
}
}
fun onWearSyncChanged(
context: Context,
enabled: Boolean,
requestPermission: () -> Unit
): Boolean {
return if (enabled) {
if (arePermissionsGranted(context)) {
start(context)
true
} else {
requestPermission()
false
}
} else {
stop(context)
true
}
}
private fun arePermissionsGranted(context: Context): Boolean {
return BluetoothPermissionHelper.isBluetoothConnectGranted(context) &&
BluetoothPermissionHelper.isPostNotificationsGranted(context)
}
fun onPermissionResult(context: Context, granted: Boolean) {
if (granted && Settings(context).wearSyncEnabled) {
start(context)
} else {
stop(context)
if (!granted) {
PreferenceManager.getDefaultSharedPreferences(context).edit {
putBoolean(context.getString(R.string.settings_key_wear_sync), false)
}
}
}
}
private fun start(context: Context) {
// Exception can only throw on API level 31 and up
if (Build.VERSION.SDK_INT >= 31) {
try {
ContextCompat.startForegroundService(
context,
Intent(context, BluetoothServerService::class.java)
)
} catch (e: ForegroundServiceStartNotAllowedException) {
// We naively assume we have enough permissions to show a notification as the service should never try to start
// before the user gave notification access anyway
Log.d(TAG, "Foreground service failed to launch: $e")
val channel = NotificationChannel(
NotificationInfo.WearBluetooth.CRITICAL_ERROR_CHANNEL_ID,
context.getString(R.string.wear_bt_critical_error_channel_name),
NotificationManager.IMPORTANCE_HIGH
).apply { setShowBadge(false) }
context.getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
val notification = NotificationCompat.Builder(context, NotificationInfo.WearBluetooth.CRITICAL_ERROR_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification_error)
.setContentTitle(context.getString(R.string.wear_bt_failed_foreground_notification_title))
.setContentText(e.message)
.build()
with(NotificationManagerCompat.from(context)) {
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
return@with
}
notify(
NotificationInfo.WearBluetooth.CRITICAL_ERROR_NOTIFICATION_ID,
notification
)
}
}
} else {
ContextCompat.startForegroundService(
context,
Intent(context, BluetoothServerService::class.java)
)
}
}
private fun stop(context: Context) {
context.stopService(Intent(context, BluetoothServerService::class.java))
}
}
File diff suppressed because one or more lines are too long.
@@ -0,0 +1,30 @@
<!--
~ 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="#FFFFFF">
<group android:scaleX="1.104"
android:scaleY="1.104"
android:translateX="-49.92"
android:translateY="-49.92">
<path
android:fillColor="@android:color/white"
android:pathData="M508.5,668.5Q520,657 520,640Q520,623 508.5,611.5Q497,600 480,600Q463,600 451.5,611.5Q440,623 440,640Q440,657 451.5,668.5Q463,680 480,680Q497,680 508.5,668.5ZM440,520L520,520L520,280L440,280L440,520ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
</group>
</vector>
@@ -0,0 +1,30 @@
<!--
~ 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="#FFFFFF">
<group android:scaleX="0.92"
android:scaleY="0.92"
android:translateX="38.4"
android:translateY="38.4">
<path
android:fillColor="@android:color/white"
android:pathData="M160,800L160,720L270,720L254,706Q202,660 181,601Q160,542 160,482Q160,371 226.5,284.5Q293,198 400,170L400,254Q328,280 284,342.5Q240,405 240,482Q240,527 257,569.5Q274,612 310,648L320,658L320,560L400,560L400,800L160,800ZM560,790L560,706Q632,680 676,617.5Q720,555 720,478Q720,433 703,390.5Q686,348 650,312L640,302L640,400L560,400L560,160L800,160L800,240L690,240L706,254Q755,303 777.5,360.5Q800,418 800,478Q800,589 733.5,675.5Q667,762 560,790Z"/>
</group>
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 919 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 B

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false">
<LinearLayout
android:id="@+id/wear_sync_device_list_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/wear_sync_device_list_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="?attr/dialogPreferredPadding"
android:paddingEnd="?attr/dialogPreferredPadding"
style="?attr/materialAlertDialogBodyTextStyle" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/listPreferredItemHeightSmall"
android:gravity="center_vertical"
android:paddingStart="?attr/listPreferredItemPaddingLeft"
android:paddingEnd="?attr/listPreferredItemPaddingRight"
android:background="?attr/selectableItemBackground"
android:ellipsize="marquee"
android:focusable="true"
android:clickable="true"
style="?attr/materialAlertDialogBodyTextStyle" />
+33 -28
View File
@@ -3,11 +3,11 @@ Branden Archer
J. Lavoie
solokot
Allan Nordhøy
Heimen Stoffels
Oğuz Ersen
Heimen Stoffels
B o d o
FC (Fay) Stegerman
大王叫我来巡山
B o d o
StoyanDimitrov
SlavekB
Katharine Chui / Katie
@@ -17,31 +17,32 @@ IllusiveMan196
Максим Горпиніч
Edgars Andersons
Liner Seven
Altonss
Fjuro
Joel A
GitSpoon
Michael Moroni
Joel A
Altonss
Priit Jõerüüt
Michael Moroni
Eric
GM
laralem
Petr Novák
laralem
Vasilis
Taco
Milo Ivir
nadiafekihahmed
pfaffenrodt
Vasilis
Aayush Gupta
damjang
Scrambled777
josé m
ikanakova
Nyatsuki
Jiri Grönroos
Giovanni Donisi
Kachelkaiser
Jiri Grönroos
HudobniVolk
109247019824
HudobniVolk
Warder
Balázs Meskó
Traductor
@@ -55,11 +56,11 @@ Feike
Arno-github
Jose Delvani
Milan Šalka
damjang
Robin
தமிழ்நேரம்
huuhaa
Skrripy
NooB9496
Govindgopalyadav
Gideon
waffshappen
@@ -68,39 +69,43 @@ JungHee Lee
pacavarn
ngocanhtve
aradxxx
Watchman89
StellarSand
Quentin PAGÈS
Projjal Moitra
Aliaksandr Trush
e-michalak
தமிழ் நேரம்
josé m.
hajertabbane
Denis Shilin
Renko
Ricky Tigg
Robin Liu
Ziad OUALHADJ
inavleb
Ziad OUALHADJ
Robin Liu
Ricky Tigg
Renko
Denis Shilin
しいたけ
Alexander Ivanov
Miha Frangež
stavpup
mrestivill
ezn24
ehrt74
Virginie
Tim Trek
Thu-Trang Pham
Tim Trek
Virginie
dukezas
ehrt74
ezn24
mrestivill
stavpup
Miha Frangež
Alì Mortacci
Andre Costa
arshbeerSingh
Augustin LAVILLE
Freddo espresso
Peter Dave Hello
PRATHAMESH BHAGAT
Michael Gangolf
rudy3
echo r"0xX4H" | rev
Kim Seohyun
Govind S Nair
Kim Seohyun
Libre
echo r"0xX4H" | rev
rudy3
Michael Gangolf
PRATHAMESH BHAGAT
Peter Dave Hello
Thomas Bertels
+4 -4
View File
@@ -16,7 +16,7 @@
<string name="edit">تعديل</string>
<string name="delete">حذف</string>
<string name="confirm">تأكيد</string>
<string name="deleteConfirmation">حذف هذه البطاقة نهائيًّا؟</string>
<string name="deleteConfirmation">هل ترغب في حذف هذه البطاقة نهائيًّا؟</string>
<string name="ok">حسنًا</string>
<string name="share">مشاركة</string>
<string name="sendLabel">إرسال…</string>
@@ -161,9 +161,9 @@
<item quantity="zero">حذف هذه <xliff:g>%d</xliff:g> البطاقة نهائيًا؟</item>
<item quantity="one">حذف هذه <xliff:g>%d</xliff:g> البطاقة نهائيًا؟</item>
<item quantity="two">حذف هذه <xliff:g>%d</xliff:g> البطاقتين نهائيًا؟</item>
<item quantity="few">حذف هذه <xliff:g>%d</xliff:g> البطاقات نهائيًا؟</item>
<item quantity="many">حذف هذه <xliff:g>%d</xliff:g> البطاقات نهائيًا؟</item>
<item quantity="other">حذف هذه <xliff:g>%d</xliff:g> البطاقات نهائيًا؟</item>
<item quantity="few">هل ترغب في حذف هذه البطاقات الـ <xliff:g>%d</xliff:g> نهائيًّا؟</item>
<item quantity="many">هل ترغب في حذف هذه البطاقات الـ <xliff:g>%d</xliff:g> نهائيًّا؟</item>
<item quantity="other">هل ترغب في حذف هذه البطاقات الـ <xliff:g>%d</xliff:g> نهائيًا؟</item>
</plurals>
<string name="importOptionFilesystemTitle">الاستيراد من نظام الملفات</string>
<string name="importOptionFilesystemExplanation">اختر ملفًا محددًا من نظام الملفات</string>
+5
View File
@@ -303,4 +303,9 @@
<string name="nfc_blocked_while_viewing_card">NFC е на пауза</string>
<string name="change_settings">Настройки</string>
<string name="nfc_block_system_error">Грешка при спиране на NFC</string>
<string name="sort_by_most_recently_added">Поредност на добавяне</string>
<string name="zoomFrontImage">Увеличаване на предното изображение</string>
<string name="zoomBackImage">Увеличаване на задното изображение</string>
<string name="show_card_id">Показване номера на картата</string>
<string name="open_in_gallery">Отваряне в галерията</string>
</resources>
+5 -5
View File
@@ -3,13 +3,13 @@
<string name="save">সংরক্ষণ</string>
<string name="cancel">বাতিল</string>
<string name="unstar">তারা মুক্ত</string>
<string name="star">তারা</string>
<string name="barcodeType">বারকোড ধরন</string>
<string name="note">বিঃদ্রঃ</string>
<string name="storeName">দোকানের নাম</string>
<string name="star">পছন্দের তালিকায় যোগ করুন</string>
<string name="barcodeType">বারকোডের ধরন</string>
<string name="note">উল্লেখ্য</string>
<string name="storeName">নাম</string>
<string name="noMatchingGiftCards">কোনো ফলাফল পাওয়া যায়নি। অনুসন্ধানের বাক্যটি বদলে দেখুন।</string>
<string name="noGiftCards">প্লাস বোতামটি টিপে একটি কার্ড যোগ করুন বা ⋮ মেনু থেকে কিছু নিয়ে আসুন।</string>
<string name="action_add">কর্ম যোগ</string>
<string name="action_add">যোগ করুন</string>
<string name="all">সব</string>
<string name="noGroupCards">এই গ্রুপটি খালি</string>
<string name="noGroups">+ যোগ বোতামটি টিপে গ্রুপ যোগ করুন যাতে তাদের শ্রেণীকরণ করা যায়।</string>
+5
View File
@@ -309,4 +309,9 @@
<string name="nfc_blocked_while_viewing_card">NFC je pozastaveno</string>
<string name="change_settings">Změnit nastavení</string>
<string name="nfc_block_system_error">Nepodařilo se pozastavit NFC</string>
<string name="sort_by_most_recently_added">Nedávno přidáno</string>
<string name="zoomFrontImage">Přiblížit přední obrázek</string>
<string name="zoomBackImage">Přiblížit zadní obrázek</string>
<string name="show_card_id">Zobrazit ID karty</string>
<string name="open_in_gallery">Otevřít v galerii</string>
</resources>
+5
View File
@@ -303,4 +303,9 @@
<string name="settings_disable_nfc_while_viewing_card_summary">Deaktiviert vorübergehend NFC, sodass keine NFC-Zahlungen ausgelöst werden können, während eine Karte angezeigt wird</string>
<string name="settings_disable_nfc_while_viewing_card">NFC vorübergehend deaktivieren</string>
<string name="nfc_block_system_error">Pausieren von NFC ist fehlgeschlagen</string>
<string name="show_card_id">Karten-ID anzeigen</string>
<string name="zoomBackImage">Rückseite vergrößern</string>
<string name="zoomFrontImage">Vorderseite vergrößern</string>
<string name="sort_by_most_recently_added">Zuletzt hinzugefügt</string>
<string name="open_in_gallery">In Bildergalerie öffnen</string>
</resources>
@@ -303,4 +303,9 @@
<string name="nfc_blocked_while_viewing_card">Το NFC είναι σε παύση</string>
<string name="change_settings">Αλλαγή ρυθμίσεων</string>
<string name="nfc_block_system_error">Αποτυχία παύσης NFC</string>
<string name="sort_by_most_recently_added">Πρόσφατα προστεθέντα</string>
<string name="zoomFrontImage">Μεγέθυνση μπροστινής εικόνας</string>
<string name="zoomBackImage">Μεγέθυνση πίσω εικόνας</string>
<string name="show_card_id">Εμφάνιση ταυτότητας κάρτας</string>
<string name="open_in_gallery">Άνοιγμα στη συλλογή εικόνων</string>
</resources>
+4
View File
@@ -309,4 +309,8 @@
<string name="nfc_blocked_while_viewing_card">NFC está pausado</string>
<string name="change_settings">Cambiar ajustes</string>
<string name="nfc_block_system_error">Incorrecto al pausar NFC</string>
<string name="sort_by_most_recently_added">Más recientemente añadido</string>
<string name="zoomFrontImage">Imagen frontal de zoom</string>
<string name="zoomBackImage">Imagen trasera de zoom</string>
<string name="show_card_id">Mostrar ID de tarjeta</string>
</resources>
+5
View File
@@ -303,4 +303,9 @@
<string name="change_settings">Muuda seadistusi</string>
<string name="settings_disable_nfc_while_viewing_card_summary">Vältimaks, et lähiväljaside (NFC) abil toimiv makseliides käivitub, lülitatakse kaardi vaatamisel lähiväljaside (NFC) välja</string>
<string name="nfc_block_system_error">Lähiväljaside (NFC) peatamine ei õnnestunud</string>
<string name="sort_by_most_recently_added">Viimasena lisatud</string>
<string name="zoomFrontImage">Suumi esikaane pilti</string>
<string name="zoomBackImage">Suumi tagakaane pilti</string>
<string name="show_card_id">Näita kaardi tunnust</string>
<string name="open_in_gallery">Ava pildigaleriis</string>
</resources>
+10
View File
@@ -299,4 +299,14 @@
<string name="back">Takaisin</string>
<string name="acra_explain_crash">Jos mahdollista lisää tietoja toiminnastasi täällä:</string>
<string name="acra_crash_email_subject"><xliff:g id="app_name">%s</xliff:g> pysähtymisraportti</string>
<string name="settings_disable_nfc_while_viewing_card">Keskeytä NFC</string>
<string name="settings_disable_nfc_while_viewing_card_summary">Keskeyttää NFC:n, jotta NFC-maksut eivät aktivoituisi korttia katsoessa</string>
<string name="sort_by_most_recently_added">Viimeksi lisätty</string>
<string name="zoomFrontImage">Suurenna etukuvaa</string>
<string name="zoomBackImage">Suurenna takakuvaa</string>
<string name="show_card_id">Näytä kortin tunniste</string>
<string name="nfc_blocked_while_viewing_card">NFC on keskeytetty</string>
<string name="change_settings">Muuta asetuksia</string>
<string name="nfc_block_system_error">NFC:n keskeyttäminen epäonnistui</string>
<string name="open_in_gallery">Avaa kuvagalleriassa</string>
</resources>
+5
View File
@@ -309,4 +309,9 @@
<string name="nfc_blocked_while_viewing_card">Le NFC est en pause</string>
<string name="change_settings">Modifier les paramètres</string>
<string name="nfc_block_system_error">Erreur lors de la mise en pause du NFC</string>
<string name="sort_by_most_recently_added">Récemment ajoutés</string>
<string name="zoomFrontImage">Grossir l\'image avant</string>
<string name="zoomBackImage">Grossir l\'image arrière</string>
<string name="show_card_id">Afficher l\'ID de carte</string>
<string name="open_in_gallery">Ouvrir dans la galerie d\'images</string>
</resources>
+5
View File
@@ -302,4 +302,9 @@
<string name="nfc_blocked_while_viewing_card">NFC está en pausa</string>
<string name="change_settings">Cambiar axustes</string>
<string name="nfc_block_system_error">Non se detivo NFC</string>
<string name="sort_by_most_recently_added">Máis recentes</string>
<string name="zoomFrontImage">Zoom na imaxe frontal</string>
<string name="zoomBackImage">Zoom na imaxe posterior</string>
<string name="show_card_id">Mostrar ID da tarxeta</string>
<string name="open_in_gallery">Abrir na galería de imaxes</string>
</resources>
@@ -297,4 +297,8 @@
<string name="nfc_blocked_while_viewing_card">NFC dijeda</string>
<string name="change_settings">Ubah pengaturan</string>
<string name="nfc_block_system_error">Gagal menjeda NFC</string>
<string name="show_card_id">Tampilkan ID Kartu</string>
<string name="sort_by_most_recently_added">Terakhir ditambahkan</string>
<string name="zoomFrontImage">Perbesar gambar depan</string>
<string name="zoomBackImage">Perbesar gambar belakang</string>
</resources>
+5
View File
@@ -309,4 +309,9 @@
<string name="nfc_blocked_while_viewing_card">La funzione NFC è disattivata</string>
<string name="change_settings">Modifica impostazioni</string>
<string name="nfc_block_system_error">Impossibile mettere in pausa l\'NFC</string>
<string name="sort_by_most_recently_added">Aggiunte più di recente</string>
<string name="show_card_id">Mostra ID carta</string>
<string name="zoomFrontImage">Zoom immagine fronte</string>
<string name="zoomBackImage">Zoom immagine retro</string>
<string name="open_in_gallery">Apri nella galleria immagini</string>
</resources>
+5
View File
@@ -296,4 +296,9 @@
<string name="nfc_blocked_while_viewing_card">NFCは停止中です</string>
<string name="change_settings">設定を変更する</string>
<string name="nfc_block_system_error">NFCの一時停止に失敗</string>
<string name="sort_by_most_recently_added">最近追加されたもの</string>
<string name="zoomFrontImage">券面を拡大</string>
<string name="zoomBackImage">背面を拡大</string>
<string name="show_card_id">カードIDを表示</string>
<string name="open_in_gallery">ギャラリーを開く</string>
</resources>
+11 -11
View File
@@ -11,10 +11,10 @@
<string name="delete">Ištrinti</string>
<string name="confirm">Patvirtinti</string>
<string name="ok">Gerai</string>
<string name="editCardTitle">Redaguoti lojalumo kortelę</string>
<string name="addCardTitle">Pridėti lojalumo kortelę</string>
<string name="editCardTitle">Redaguoti kortelę</string>
<string name="addCardTitle">Pridėti kortelę</string>
<string name="scanCardBarcode">Nuskaityti brūkšninį kodą</string>
<string name="importExport">Importuoti/Exportuoti</string>
<string name="importExport">Importas/Exportas</string>
<string name="exportName">Exportuoti</string>
<string name="importFailed">Nepavyko importuoti</string>
<string name="exportFailed">Nepavyko eksportuoti</string>
@@ -43,7 +43,7 @@
<string name="app_loyalty_card_keychain">Lojalumo kortelės</string>
<string name="moveBarcodeToTopOfScreen">Perkelti brūkšninį kodą į ekrano viršų</string>
<string name="settings_display_barcode_max_brightness">Šviesinti ekraną</string>
<string name="failedParsingImportUriError">Nepavyko iššifruoti importo URI</string>
<string name="failedParsingImportUriError">Nepavyko nuskaityti importo URI</string>
<string name="turn_flashlight_off">Išjunkti žibintuvėlį</string>
<string name="turn_flashlight_on">Įjunkti žibintuvėlį</string>
<string name="failedGeneratingShareURL">Nepavyko sugeneruoti bendrinamo URL</string>
@@ -65,11 +65,9 @@
<string name="setBarcodeId">Nustatyti brūkšninio kodo reikšmę</string>
<string name="sameAsCardId">Tokia pat kaip ID</string>
<string name="barcodeId">Brūkšninio kodo reikšmė</string>
<string name="importFidmeMessage">Pasirinkite <i>fidme-export-request-xxxxxx.zip</i> eksportą iš FidMe, kurį norite importuoti, ir po to brūkšninių kodų tipus pasirinkite rankiniu būdu.
\nSukurkite jį iš savo FidMe profilio, pasirinkę Duomenų apsauga ir pirmiausia paspaudę Išgauti mano duomenis.</string>
<string name="importFidmeMessage">Importavimui pasirinkite eksportą iš FidMe ir po to brūkšninių kodų tipus pasirinkite rankiniu būdu. \nSukurkite jį iš savo FidMe profilio pasirinkę \"Duomenų apsauga\" ir po to paspausdami \"Išgauti mano duomenis\".</string>
<string name="importFidme">Importuoti iš FidMe</string>
<string name="importCatimaMessage">Norėdami importuoti, pasirinkite savo <i> catima.zip </i> eksportavimą iš Catima.
\nSukurkite ją iš kitos Catima programos importavimo / eksportavimo meniu, pirmiausia ten paspausdami Eksportuoti.</string>
<string name="importCatimaMessage">Norėdami importuoti, pasirinkite savo <i> catima.zip </i> eksportavimą iš Catima. \nSukurkite ją iš kitos Catima programos importavimo/eksportavimo meniu paspausdami \"Eksportuoti\".</string>
<string name="importCatima">Importuoti iš Catima</string>
<string name="accept">Priimti</string>
<string name="privacy_policy">Privatumo politika</string>
@@ -117,8 +115,8 @@
<string name="settings">Nustatymai</string>
<string name="starImage">Mėgstamiausia žvaigždė</string>
<string name="thumbnailDescription">Miniatiūra</string>
<string name="app_resources">Laisvosios trečiųjų šalių ištekliai: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Laisvosios trečiųjų šalių bibliotekos: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_resources">Trečiųjų šalių ištekliai: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Trečiųjų šalių bibliotekos: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_copyright_old">Paremta Loyalty Card Keychain
\nautorinės teisės © 20162020 Branden Archer</string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Autorinės teisės © 2019<xliff:g>%d</xliff:g> Sylvia van Os</string>
@@ -248,7 +246,7 @@
<string name="add_manually_warning_title">Rekomenduojama skenuoti</string>
<string name="continue_">Tęsti</string>
<string name="add_manually_warning_message">Kai kuriuose parduotuvėse brūkšninio kodo vertė gali skirtis nuo kortelėje nurodyto numerio. Dėlto įvedus brūkšninį kodą rankiniu būdu gali nepavykti. Vietoj to, labai rekomenduojama nuskaityti brūkšninį kodą naudojant kamerą. Ar vis tiek norite tęsti?</string>
<string name="permissionReadCardsLabel">Skenuoti Catima Korteles</string>
<string name="permissionReadCardsLabel">Skaityti Catima korteles</string>
<string name="settings_category_title_cards">Kortelių peržiūra</string>
<string name="settings_category_title_general">Bendri</string>
<string name="settings_category_title_privacy">Privatumąi</string>
@@ -281,4 +279,6 @@
<string name="settings_column_count_portrait">Stulpeliai vertikaliame režime</string>
<string name="settings_column_count_landscape">Stulpeliai horizontaliame režime</string>
<string name="settings_automatic_column_count">Automatiškai</string>
<string name="settings_disable_nfc_while_viewing_card">NFC pauzė</string>
<string name="settings_disable_nfc_while_viewing_card_summary">Pristabdo NFC, kad peržiūrint kortelę nebūtų suaktyvinti NFC mokėjimai</string>
</resources>
+5
View File
@@ -309,4 +309,9 @@
<string name="settings_disable_nfc_while_viewing_card_summary">Aptur NFC, lai novērstu NFC maksājumu izsaukšanu kartes apskatīšanas laikā</string>
<string name="nfc_blocked_while_viewing_card">NFC ir apturēts</string>
<string name="change_settings">Mainīt iestatījumus</string>
<string name="open_in_gallery">Atvērt attēlu galerijā</string>
<string name="sort_by_most_recently_added">Visnesenāk pievienotās</string>
<string name="zoomFrontImage">Pietuvināt priekšpuses attēlu</string>
<string name="zoomBackImage">Pietuvināt aizmugures attēlu</string>
<string name="show_card_id">Rādīt kartes Id</string>
</resources>
+5
View File
@@ -303,4 +303,9 @@
<string name="settings_disable_nfc_while_viewing_card_summary">Pauzeert NFC om te voorkomen dat NFC-betalingen activeren tijdens het bekijken van een kaart</string>
<string name="nfc_blocked_while_viewing_card">NFC is gepauzeerd</string>
<string name="nfc_block_system_error">Pauzeren van NFC mislukt</string>
<string name="sort_by_most_recently_added">Op onlangs toegevoegd</string>
<string name="show_card_id">Kaart ID tonen</string>
<string name="zoomFrontImage">Voorzijde inzoomen</string>
<string name="zoomBackImage">Achterzijde inzoomen</string>
<string name="open_in_gallery">Open in afbeeldingsgalerij</string>
</resources>
+5
View File
@@ -315,4 +315,9 @@
<string name="nfc_blocked_while_viewing_card">NFC zostało zatrzymane</string>
<string name="change_settings">Zmień ustawienia</string>
<string name="nfc_block_system_error">Błąd podczas zatrzymania NFC</string>
<string name="sort_by_most_recently_added">Najnowsze dodane</string>
<string name="zoomFrontImage">Powiększ zdjęcie z przodu</string>
<string name="zoomBackImage">Powiększ zdjęcie z tyłu</string>
<string name="show_card_id">Pokaż ID karty</string>
<string name="open_in_gallery">Otwórz zdjęcie w galerii</string>
</resources>
@@ -309,4 +309,9 @@
<string name="nfc_blocked_while_viewing_card">NFC está pausado</string>
<string name="change_settings">Alterar configurações</string>
<string name="nfc_block_system_error">Falha ao pausar o NFC</string>
<string name="sort_by_most_recently_added">Adicionados mais recentemente</string>
<string name="zoomFrontImage">Ampliar imagem frontal</string>
<string name="zoomBackImage">Reduzir o zoom da imagem</string>
<string name="show_card_id">Mostrar ID do cartão</string>
<string name="open_in_gallery">Abrir na galeria de imagens</string>
</resources>
@@ -309,4 +309,6 @@
<string name="nfc_blocked_while_viewing_card">NFC está pausado</string>
<string name="change_settings">Alterar configurações</string>
<string name="nfc_block_system_error">Falha ao pausar o NFC</string>
<string name="zoomFrontImage">Ampliar imagem frontal</string>
<string name="show_card_id">Mostrar ID do cartão</string>
</resources>
+3
View File
@@ -308,4 +308,7 @@
<string name="nfc_blocked_while_viewing_card">NFC está pausado</string>
<string name="change_settings">Alterar configurações</string>
<string name="nfc_block_system_error">Erro ao pausar o NFC</string>
<string name="zoomFrontImage">Ampliar imagem frontal</string>
<string name="show_card_id">Mostrar ID do cartão</string>
<string name="sort_by_most_recently_added">Adicionados mais recentemente</string>
</resources>
+5
View File
@@ -315,4 +315,9 @@
<string name="nfc_blocked_while_viewing_card">NFC приостановлен</string>
<string name="change_settings">Изменить настройки</string>
<string name="nfc_block_system_error">Приостановка NFC невозможна</string>
<string name="sort_by_most_recently_added">Порядок добавления</string>
<string name="show_card_id">Показывать номер карты</string>
<string name="zoomFrontImage">Масштабирование лицевого изображения</string>
<string name="zoomBackImage">Масштабирование заднего изображения</string>
<string name="open_in_gallery">Открыть в галерее</string>
</resources>
+5
View File
@@ -314,4 +314,9 @@
<string name="nfc_blocked_while_viewing_card">NFC je zaustavljen</string>
<string name="change_settings">Spremeni nastavitve</string>
<string name="nfc_block_system_error">ustavitev NFC-ja ni uspela</string>
<string name="sort_by_most_recently_added">Nazadnje dodane</string>
<string name="zoomFrontImage">Povečaj sprednjo sliko</string>
<string name="zoomBackImage">Povečaj zadnjo sliko</string>
<string name="show_card_id">Prikaži ID kartice</string>
<string name="open_in_gallery">Odpri v galeriji slik</string>
</resources>
+5
View File
@@ -303,4 +303,9 @@
<string name="change_settings">Ändra inställningar</string>
<string name="nfc_block_system_error">Lyckades inte pausa NFC</string>
<string name="settings_disable_nfc_while_viewing_card_summary">Pausar NFC för att förhindra att NFC-betalningar genomförs av misstag medan du tittar på ett kort</string>
<string name="sort_by_most_recently_added">Senast tillagda</string>
<string name="zoomFrontImage">Zooma framsidesbilden</string>
<string name="zoomBackImage">Zooma baksidesbilden</string>
<string name="show_card_id">Visa kort-ID</string>
<string name="open_in_gallery">Öppna i bildgalleriet</string>
</resources>
+5
View File
@@ -303,4 +303,9 @@
<string name="nfc_blocked_while_viewing_card">NFC duraklatıldı</string>
<string name="change_settings">Ayarları değiştir</string>
<string name="nfc_block_system_error">NFC duraklatılamadı</string>
<string name="sort_by_most_recently_added">En son eklenen</string>
<string name="zoomFrontImage">Ön görüntüyü yakınlaştır</string>
<string name="zoomBackImage">Arka görüntüyü yakınlaştır</string>
<string name="show_card_id">Kart numarasını göster</string>
<string name="open_in_gallery">Resim galerisinde aç</string>
</resources>
+5
View File
@@ -297,4 +297,9 @@
<string name="nfc_blocked_while_viewing_card">Đã tạm dừng NFC</string>
<string name="change_settings">Thay đổi cài đặt</string>
<string name="nfc_block_system_error">Không thể tạm dừng NFC</string>
<string name="sort_by_most_recently_added">Đã thêm gần đây nhất</string>
<string name="zoomFrontImage">Phóng ảnh mặt trước</string>
<string name="zoomBackImage">Phóng ảnh mặt sau</string>
<string name="show_card_id">Hiện ID của thẻ</string>
<string name="open_in_gallery">Mở trong thư viện ảnh</string>
</resources>
@@ -297,4 +297,9 @@
<string name="nfc_blocked_while_viewing_card">NFC 已暂停</string>
<string name="change_settings">更改设置</string>
<string name="nfc_block_system_error">暂停 NFC 失败</string>
<string name="sort_by_most_recently_added">最近添加</string>
<string name="zoomFrontImage">缩放正面图</string>
<string name="zoomBackImage">缩放反面图</string>
<string name="show_card_id">显示卡片 ID</string>
<string name="open_in_gallery">在图库中打开</string>
</resources>
+44 -2
View File
@@ -104,8 +104,8 @@
<string name="settings_disable_nfc_while_viewing_card">Pause NFC</string>
<string name="settings_disable_nfc_while_viewing_card_summary">Pauses NFC to prevent NFC payments from triggering while viewing a card</string>
<string name="settings_key_disable_nfc_while_viewing_card" translatable="false">pref_disable_nfc_while_viewing_card</string>
<string name="settings_allow_content_provider_read_title">Allow other apps to access my data</string>
<string name="settings_allow_content_provider_read_summary">Apps will still have to request permission to be granted access</string>
<string name="settings_allow_content_provider_read_title">Sync with Gadgetbridge</string>
<string name="settings_allow_content_provider_read_summary">Allows the Gadgetbridge app to sync your cards to supported watches</string>
<string name="settings_key_disable_lockscreen_while_viewing_card" translatable="false">pref_disable_lockscreen_while_viewing_card</string>
<string name="settings_key_allow_content_provider_read" translatable="false">pref_allow_content_provider_read</string>
<string name="settings_key_oled_dark" translatable="false">pref_oled_dark</string>
@@ -304,6 +304,42 @@
<string name="setting_key_column_count_landscape" translatable="false">pref_column_count_landscape</string>
<string name="settings_category_title_general">General</string>
<string name="settings_category_title_privacy">Privacy</string>
<string name="settings_key_wear_sync" translatable="false">pref_wear_sync</string>
<string name="settings_wear_sync">Sync with Wear OS</string>
<string name="settings_wear_sync_summary">Allows the Wear OS companion app to read your cards over Bluetooth</string>
<string name="wear_sync_permission_required">Catima needs the permissions you denied to sync with your Wear OS smartwatch</string>
<string name="settings_key_wear_sync_devices" translatable="false">pref_wear_sync_devices</string>
<string name="settings_wear_sync_devices">Allowed Wear OS devices</string>
<string name="settings_wear_sync_devices_summary">Manage devices allowed to sync over Bluetooth</string>
<string name="settings_wear_sync_no_devices">No allowed devices</string>
<plurals name="settings_wear_sync_devices_summary_count">
<item quantity="one"><xliff:g>%d</xliff:g> device allowed</item>
<item quantity="other"><xliff:g>%d</xliff:g> devices allowed</item>
</plurals>
<string name="settings_key_wear_sync_blocked_devices" translatable="false">pref_wear_sync_blocked_devices</string>
<string name="settings_wear_sync_blocked_devices">Blocked Wear OS devices</string>
<string name="settings_wear_sync_blocked_devices_summary">Manage blocked Wear OS devices</string>
<string name="settings_wear_sync_no_blocked_devices">No blocked devices</string>
<plurals name="settings_wear_sync_blocked_devices_summary_count">
<item quantity="one"><xliff:g>%d</xliff:g> device blocked</item>
<item quantity="other"><xliff:g>%d</xliff:g> devices blocked</item>
</plurals>
<string name="settings_wear_sync_unpair_title">Remove device?</string>
<string name="settings_wear_sync_unpair_message">Remove <xliff:g>%1$s</xliff:g> from Catima? It will no longer be able to sync your cards.</string>
<string name="settings_wear_sync_unblock_message">Remove <xliff:g>%1$s</xliff:g> from the blocked list? It will be able to request access again.</string>
<string name="settings_wear_sync_unpair">Remove</string>
<string name="settings_wear_sync_device_name"><xliff:g>%1$s</xliff:g> (<xliff:g>%2$s</xliff:g>)</string>
<string name="settings_wear_sync_tap_to_remove">Tap a device to remove it.</string>
<string name="wear_bt_pairing_channel_name">Wear OS device pairing</string>
<string name="wear_bt_pairing_title">Wear OS device wants to sync</string>
<string name="wear_bt_pairing_message">Allow <xliff:g>%1$s</xliff:g> to access your cards?</string>
<string name="wear_bt_pairing_allow">Allow</string>
<string name="wear_bt_pairing_block">Block</string>
<string name="wear_bt_pairing_allowed"><xliff:g>%1$s</xliff:g> is now allowed to sync.</string>
<string name="wear_bt_pairing_blocked"><xliff:g>%1$s</xliff:g> has been blocked.</string>
<string name="wear_bt_pairing_mismatch_title">New authorization request from "<xliff:g>%1$s</xliff:g>"</string>
<string name="wear_bt_pairing_mismatch_message">This device was previously authorized but is now presenting a different identity token. This happens when the Wear OS app is reinstalled or its data is cleared. It can also indicate a Bluetooth MAC spoofing attempt.\n\nOnly accept if you recently reinstalled the Catima Wear OS app on this watch.</string>
<string name="open_settings">Open settings</string>
<string name="action_display_options">Display options</string>
<string name="show_archived_cards">Show archived cards</string>
<string name="view_online">View online</string>
@@ -353,4 +389,10 @@
<string name="nfc_blocked_while_viewing_card">NFC is paused</string>
<string name="change_settings">Change settings</string>
<string name="nfc_block_system_error">Failed to pause NFC</string>
<string name="open_in_gallery">Open in image gallery</string>
<string name="wear_bt_channel_name">Wear OS companion</string>
<string name="wear_bt_notification_title">Catima Wear OS sync active</string>
<string name="settings_category_title_smartwatch">Smartwatch support</string>
<string name="wear_bt_failed_foreground_notification_title">Failed to start Wear OS sync service</string>
<string name="wear_bt_critical_error_channel_name">Wear OS sync critical error</string>
</resources>
+7
View File
@@ -57,6 +57,13 @@
<item name="postSplashScreenTheme">@style/AppTheme</item>
</style>
<style name="AppTheme.Transparent" parent="AppTheme.NoActionBar">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:backgroundDimEnabled">true</item>
</style>
<!-- place holder for dark background night theme overrides -->
<style name="DarkBackground" />
</resources>
+24 -1
View File
@@ -121,7 +121,7 @@
</PreferenceCategory>
<PreferenceCategory
android:title="@string/settings_category_title_privacy"
android:title="@string/settings_category_title_smartwatch"
app:iconSpaceReserved="false">
<SwitchPreferenceCompat
@@ -132,6 +132,29 @@
android:title="@string/settings_allow_content_provider_read_title"
app:iconSpaceReserved="false"
app:singleLineTitle="false" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preference_material_switch"
android:defaultValue="false"
android:key="@string/settings_key_wear_sync"
android:summary="@string/settings_wear_sync_summary"
android:title="@string/settings_wear_sync"
app:iconSpaceReserved="false"
app:singleLineTitle="false" />
<Preference
android:key="@string/settings_key_wear_sync_devices"
android:title="@string/settings_wear_sync_devices"
android:summary="@string/settings_wear_sync_devices_summary"
app:iconSpaceReserved="false"
app:singleLineTitle="false" />
<Preference
android:key="@string/settings_key_wear_sync_blocked_devices"
android:title="@string/settings_wear_sync_blocked_devices"
android:summary="@string/settings_wear_sync_blocked_devices_summary"
app:iconSpaceReserved="false"
app:singleLineTitle="false" />
</PreferenceCategory>
</PreferenceScreen>
@@ -11,6 +11,8 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import protect.card_locker.shared.ForegroundColorHelper;
@RunWith(RobolectricTestRunner.class)
public class UtilsTest {
@@ -26,7 +28,7 @@ public class UtilsTest {
for (int i = 0; i < colors.length(); i++) {
// Grab white as fallback so that if the retrieval somehow fails the test is guaranteed to fail because a white background will have black foreground
int color = colors.getColor(i, Color.WHITE);
assertFalse(Utils.needsDarkForeground(color));
assertFalse(ForegroundColorHelper.Companion.needsDarkForeground(color));
}
});
}
@@ -0,0 +1,37 @@
package protect.card_locker.wearos
import android.content.Context
import androidx.preference.PreferenceManager
import androidx.test.core.app.ApplicationProvider
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import protect.card_locker.R
@RunWith(RobolectricTestRunner::class)
class WearSyncServiceManagerTest {
@Test
fun onPermissionResult_denied_disablesWearSync() {
val context = ApplicationProvider.getApplicationContext<Context>()
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
prefs.edit().putBoolean(context.getString(R.string.settings_key_wear_sync), true).commit()
WearSyncServiceManager.onPermissionResult(context, false)
assertFalse(prefs.getBoolean(context.getString(R.string.settings_key_wear_sync), true))
}
@Test
fun onPermissionResult_granted_keepsWearSyncEnabled() {
val context = ApplicationProvider.getApplicationContext<Context>()
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
prefs.edit().putBoolean(context.getString(R.string.settings_key_wear_sync), true).commit()
WearSyncServiceManager.onPermissionResult(context, true)
assertTrue(prefs.getBoolean(context.getString(R.string.settings_key_wear_sync), false))
}
}
+2
View File
@@ -2,7 +2,9 @@
plugins {
alias(libs.plugins.com.android.application) apply false
alias(libs.plugins.com.android.library) apply false
alias(libs.plugins.org.jetbrains.kotlin.android) apply false
alias(libs.plugins.org.jetbrains.kotlin.plugin.compose) apply false
}
allprojects {
+36 -8
View File
@@ -5,6 +5,16 @@ IFS=$'\n\t'
### build.sh
### Builds Catima the same way rbtlog/IzzyOnDroid does for reproducible builds
if [ -z "${BUILD_TYPE:-}" ]; then
echo "BUILD_TYPE is not set, setting to app."
export BUILD_TYPE="app"
fi
if [ "${BUILD_TYPE}" != "app" ] && [ "${BUILD_TYPE}" != "wear" ]; then
echo "Invalid BUILD_TYPE ${BUILD_TYPE}, must be either app or wear."
exit 1
fi
if [ -z "${ANDROID_HOME:-}" ]; then
echo "ANDROID_HOME is not set, setting to $HOME/Android/Sdk";
export ANDROID_HOME=$HOME/Android/Sdk
@@ -22,13 +32,17 @@ if [ -z "${JAVA_HOME:-}" ]; then
fi
echo "Starting build"
./gradlew clean assembleRelease
./gradlew clean ":${BUILD_TYPE}:assembleRelease"
echo "Build finished (unsigned)"
flavourDirs=$(find app/build/outputs/apk/ -mindepth 1 -maxdepth 1 -type d)
flavourDirs=$(find "${BUILD_TYPE}/build/outputs/apk/" -mindepth 1 -maxdepth 1 -type d)
for flavourDir in $flavourDirs; do
flavourName="$(basename "$flavourDir")"
echo "Your $flavourName flavour is at $flavourDir/release/app-$flavourName-release-unsigned.apk"
if [ "$flavourName" == "release" ]; then
echo "Your $flavourName flavour is at $flavourDir/${BUILD_TYPE}-release-unsigned.apk"
else
echo "Your $flavourName flavour is at $flavourDir/release/${BUILD_TYPE}-$flavourName-release-unsigned.apk"
fi
done
if [ -z "${KEYSTORE:-}" ]; then
@@ -44,19 +58,33 @@ else
for flavourDir in $flavourDirs; do
flavourName="$(basename "$flavourDir")"
echo "Signing $flavourName flavour..."
cp "$flavourDir/release/app-$flavourName-release-unsigned.apk" "$flavourDir/release/app-$flavourName-release.apk"
"$ANDROID_HOME/build-tools/$apksigner_version/apksigner" sign -v --ks "$KEYSTORE" --ks-key-alias "$KEYSTORE_ALIAS" "$flavourDir/release/app-$flavourName-release.apk"
if [ "$flavourName" == "release" ]; then
cp "$flavourDir/${BUILD_TYPE}-release-unsigned.apk" "$flavourDir/${BUILD_TYPE}-release.apk"
"$ANDROID_HOME/build-tools/$apksigner_version/apksigner" sign -v --ks "$KEYSTORE" --ks-key-alias "$KEYSTORE_ALIAS" "$flavourDir/${BUILD_TYPE}-release.apk"
else
cp "$flavourDir/release/${BUILD_TYPE}-$flavourName-release-unsigned.apk" "$flavourDir/release/${BUILD_TYPE}-$flavourName-release.apk"
"$ANDROID_HOME/build-tools/$apksigner_version/apksigner" sign -v --ks "$KEYSTORE" --ks-key-alias "$KEYSTORE_ALIAS" "$flavourDir/release/${BUILD_TYPE}-$flavourName-release.apk"
fi
echo "Build finished (signed)"
echo "Your $flavourName flavour is at $flavourDir/release/app-$flavourName-release.apk"
if [ "$flavourName" == "release" ]; then
echo "Your $flavourName flavour is at $flavourDir/${BUILD_TYPE}-release.apk"
else
echo "Your $flavourName flavour is at $flavourDir/release/${BUILD_TYPE}-$flavourName-release.apk"
fi
done
shasumPath="$(pwd)/SHA256SUMS"
echo "" > "$shasumPath"
for flavourDir in $flavourDirs; do
pushd "$flavourDir/release/"
sha256sum -- *.apk >> "$shasumPath"
flavourName="$(basename "$flavourDir")"
if [ "$flavourName" == "release" ]; then
pushd "$flavourDir"
else
pushd "$flavourDir/release/"
fi
sha256sum -- "${BUILD_TYPE}"-*.apk >> "$shasumPath"
popd
done
+97
View File
@@ -0,0 +1,97 @@
# Bluetooth server (for WearOS support and other Bluetooth devices)
Catima implements a [Rfcomm Bluetooth server](https://developer.android.com/reference/android/bluetooth/BluetoothAdapter#listenUsingRfcommWithServiceRecord(java.lang.String,%20java.util.UUID)) to allow Bluetooth devices to interact with the cards database. Right now, this only provides read-only functionality.
The UUID of the Rfcomm Bluetooth server is `e5b4f020-3a7e-4b6d-9f2c-1a8c5d3e7f90`. You should bind to this using [createRfcommSocketToServiceRecord](https://developer.android.com/reference/android/bluetooth/BluetoothDevice#createRfcommSocketToServiceRecord(java.util.UUID)) on Android or, if not Android, whatever is the alternative for your platform. Insecure Rfcomm sockets are not supported.
Since running the Bluetooth server requires extra permissions (Bluetooth and Notifications) and runs a constant background process it is disabled by default.
For security, Catima keeps a list of known MAC addresses and tokens and will only allow devices explicitly allowed by the user to interact with the Bluetooth server. This prevents every paired Bluetooth device from being able to access cards without user authentication.
The Bluetooth server can be controlled with the setting "Sync with Wear OS" under "Smartwatch support". It is disabled by default.
The Bluetooth server is currently only used by the Catima WearOS companion app. If your app/device uses it, please let us know, so we can update the documentation and contact your project in case of planned breaking changes.
## API versions
For each API version, we have a major and a minor version. The rules for versioning are as follows:
1. A new minor version may add fields or endpoints, but must never modify or remove existing fields or endpoints.
2. A new minor version must not change any authentication systems compared to the previous minor version.
3. A device cannot request Catima to use a specific minor version, it will always return data as per the most recently supported minor version.
The main driver of compatibility is the major version.
Due to this system, a device only supporting API version 1.0 can talk to Catima supporting API version 1.1 with no issues, as long as it ignores fields it doesn't know. On the other side, a device wanting to use API version 1.1 with Catima on API version 1.0 has to accept certain fields may not be available. Which minor version introduces which additional fields and endpoints is documented in this file.
### Unversioned
#### /VERSIONS
Return type: JSON.
Returns all supported major versions and the most recent supported minor version.
Example request:
```
/VERSIONS
```
Example return value:
```
["1.0"]
```
In this example case, the phone supports only API version 1. Of API version 1, the most recent minor version supported is 0.
### V1
All V1 endpoints are authenticated and must be communicated with by sending 2 lines over an RfConn socket. First the full command, then a device-specific token (recommended: a base64-encoded version of a 32 characters long SecureRandom-generated string).
The device-specific token must be generated once on the companion device and never changed.
For example, to call the first page of `/V1/CARDS_REQUEST_PAGE/` you should send:
```
/V1/CARDS_REQUEST_PAGE/0
TOKEN: your_base64_encoded_token_here
```
#### /V1/CARDS_REQUEST_PAGE/{id:int}
Return type: JSON.
Request a single page of cards with page and total page details. The page ID is 0-indexed, making 0 the first page. Currently, a page is 10 cards but this may change in the future.
Each card contains the following fields:
| Column | Type | Description | Introduced in minor version |
|-------------------|-------------------|----------------------------| --------------------------- |
| `id` | `int` | Unique card ID | 0 |
| `store` | `String` | Card name | 0 |
| `cardId` | `String` | Card ID. | 0 |
| `barcodeId` | `Nullable String` | Barcode value. If empty, it's the same as the card ID. | 0 |
| `barcodeType` | `Nullable String` | The barcode type name, matching [com.google.zxing.BarcodeFormat](https://zxing.github.io/zxing/apidocs/com/google/zxing/BarcodeFormat.html). When null, there is no barcode for this card. | 0 |
| `headerColor` | `int` | Header color, matching [Android color int](https://developer.android.com/reference/android/graphics/Color). | 0 |
Example request:
```
/V1/CARDS_REQUEST_PAGE/0
TOKEN: your_base64_encoded_token_here
```
Example return value:
```
{
"page": 0,
"totalPages": 1,
"cards": [
{
"id": 1,
"store": "Bookshop",
"cardId": "123456",
"barcodeId": null,
"barcodeType": "QR_CODE",
"headerColor": -1
}
]
}
```
+22 -18
View File
@@ -1,9 +1,13 @@
# Content Provider
# Content Provider (for Gadgetbridge and other local apps)
Catima implements a [content provider](https://developer.android.com/guide/topics/providers/content-providers) to allow for external apps to interact with the cards database. Right now, this only provides read-only functionality.
Since runtime permissions are only available since Android 6.0 (API level 23), the content provider is disabled for older android versions in order to prevent unwanted access to the data.
The content provider can be controlled with the setting "Sync with Gadgetbridge" under "Smartwatch support". It is enabled by default.
The content provider is currently only used by Gadgetbridge. If your app uses it, please let us know, so we can update the documentation and contact your project in case of planned breaking changes.
## Package Names
There are 3 release channels, with 2 possible package names:
@@ -41,23 +45,23 @@ A major version change implies breaking changes (eg. columns being renamed or re
### /cards
| Column | Type | Description |
|-------------------|----------|----------------------------|
| `_id` | `int` | Unique card ID |
| `store` | `String` | Card name |
| `validfrom` | `long` | Timestamp from which the card is valid (unix epoch millis). |
| `expiry` | `long` | Expiration timestamp (unix epoch millis). |
| `balance` | `String` | Current balance, as a string-formatted big decimal. |
| `balancetype` | `String` | Balance currency code, ISO 4217. |
| `note` | `String` | A note. |
| `headercolor` | `int` | Header color, in RGBA. |
| `cardid` | `String` | Card ID. |
| `barcodeid` | `String` | Barcode value. If empty, it's the same as the card ID. |
| `barcodetype` | `String` | The barcode type name, matching [com.google.zxing.BarcodeFormat](https://zxing.github.io/zxing/apidocs/com/google/zxing/BarcodeFormat.html). |
| `barcodeencoding` | `String` | The barcode encoding used to render the barcode, matching a [StandardCharset](https://developer.android.com/reference/java/nio/charset/StandardCharsets)'s `.name()` output. Only `ISO-8859-1` and `UTF-8` are user selectable. |
| `starstatus` | `int` | 1 if starred, 0 if not |
| `lastused` | `long` | Timestamp of last card usage (unix epoch millis). |
| `archive` | `int` | 1 if archived, 0 if not |
| Column | Type | Description |
|-------------------|-------------------|----------------------------|
| `_id` | `int` | Unique card ID |
| `store` | `String` | Card name |
| `validfrom` | `long` | Timestamp from which the card is valid (unix epoch millis). |
| `expiry` | `long` | Expiration timestamp (unix epoch millis). |
| `balance` | `String` | Current balance, as a string-formatted big decimal. |
| `balancetype` | `Nullable String` | Balance currency code, ISO 4217. When null, it's a generic "Points" indicator. |
| `note` | `String` | A note. |
| `headercolor` | `int` | Header color, matching [Android color int](https://developer.android.com/reference/android/graphics/Color). |
| `cardid` | `String` | Card ID. |
| `barcodeid` | `Nullable String` | Barcode value. If null, it's the same as the card ID. |
| `barcodetype` | `Nullable String` | The barcode type name, matching [com.google.zxing.BarcodeFormat](https://zxing.github.io/zxing/apidocs/com/google/zxing/BarcodeFormat.html). When null, there is no barcode for this card. |
| `barcodeencoding` | `String` | The barcode encoding used to render the barcode, matching a [StandardCharset](https://developer.android.com/reference/java/nio/charset/StandardCharsets)'s `.name()` output. Only `ISO-8859-1` and `UTF-8` are user selectable. |
| `starstatus` | `int` | 1 if starred, 0 if not |
| `lastused` | `long` | Timestamp of last card usage (unix epoch millis). |
| `archive` | `int` | 1 if archived, 0 if not |
### /groups
+22 -5
View File
@@ -1,14 +1,31 @@
**This documentation is for maintainers. If you're a user, please ignore it.**
# General versioning note
Android releases should be at least versionCode 1000 and end on an even number (the uneven numbers are used for F-Droid legacy builds). Valid: 1002, 1004, etc.
WearOS releases should be between 200 and 999 and only have to be bumped by 1. Valid: 200, 201, etc.
Version codes below 200 are from the pre-WearOS days.
# When releasing, do the following:
1. Press "Commit" and "Push" on Weblate to ensure all translations are up to date
2. Merge Weblate pull request
3. Make sure to pull the `main` branch locally
4. Update `CHANGELOG.md` with the new version name and the release date
5. Update `app/build.gradle.kts` with the new `versionCode` and `versionName`
6. Create a commit for the new release: `git add CHANGELOG.md app/build.gradle.kts && git commit -m "Release Catima <VERSION>"`
7. Build the new .apks: `KEYSTORE=/path/to/keystore KEYSTORE_ALIAS=catima ./build.sh`
8. Upload `app/build/outputs/apk/gplay/release/app-gplay-release.apk` to Google Play Open Testing
5. Update the relevant build.gradle.kts with the new `versionCode` and `versionName`
- For Android releases, this is `app/build.gradle.kts`, make sure to use an even number above 1000
- For Wear OS releases, this is `wear/build.gradle.kts`, make sure to use a number between 200 and 1000
6. Create a commit for the new release:
- For Android releases, `git add CHANGELOG.md app/build.gradle.kts && git commit -m "Release Catima <VERSION>"`
- For Wear OS releases, `git add CHANGELOG.md wear/build.gradle.kts && git commit -m "Release Catima Wear OS <VERSION>"`
7. Build the new .apks:
- For Android releases, `BUILD_TYPE=app KEYSTORE=/path/to/keystore KEYSTORE_ALIAS=catima ./build.sh`
- For Wear OS releases, `BUILD_TYPE=wear KEYSTORE=/path/to/keystore KEYSTORE_ALIAS=catima ./build.sh`
8. Upload the Google release to Google Play Open Testing
- For Android releases, this is `app/build/outputs/apk/gplay/release/app-gplay-release.apk`
- For Wear OS releases, this is `wear/build/outputs/apk/release/wear-release.apk` (Wear OS currently does not use flavours)
9. Push the version update commit: `git push`
10. Create a new release on GitHub and attach the `app/build/outputs/apk/foss/release/app-foss-release.apk` and `SHA256SUMS` files
10. Create a new release on GitHub and attach the non-Google APKs and `SHA256SUMS` files
- For Android releases, this is `app/build/outputs/apk/foss/release/app-foss-release.apk` and `SHA256SUMS`. Releases are tagged vX.X.X.
- For Wear OS releases, this is `wear/build/outputs/apk/release/wear-release.apk` and `SHA256SUMs` (Wear OS currently does not use flavours). Releases are tagged vX.X.X-wearos.
11. When pushing the release to Google Play Production, update the metadata there: `bundle exec fastlane supply --version_code <VERSION_CODE>`
@@ -0,0 +1,3 @@
- Přidána podpora zobrazení ID karty v seznamu karet
- Přidána podpora řazení dle nedávno přidaných
- Přidán interní prohlížeč obrázků pro přibližování předních/zadních/ikonových obrázků
@@ -0,0 +1 @@
- Přidána možnost otevření obrázku v interním prohlížeči obrázků v galerii
@@ -0,0 +1,3 @@
- NEU: Anzeige der Karten-ID in der Kartenliste
- NEU: Sortierung nach „zuletzt hinzugefügt“
- NEU: Interner Bildbetrachter zum Zoomen der Vorder- und Rückseitenbilder sowie der Icons einer Karte
@@ -0,0 +1 @@
- Option hinzugefügt, um Bilder aus dem integrierten Bildbetrachter in der Galerie App zu öffnen
@@ -0,0 +1,2 @@
- Add a Bluetooth server for Wear OS companion app support
- Remove embedded CHANGELOG
@@ -0,0 +1 @@
- Add option to open image in internal image viewer in gallery
@@ -0,0 +1 @@
- Initial release of Wear OS companion app
@@ -11,6 +11,7 @@ With this essential everyday carry (EDC) tool you can replace useless plastic wi
- Import cards and codes from files, Catima, FidMe, Loyalty Card Keychain and Voucher Vault.
- Make a backup of all your cards and transfer them to a new device if you want.
- Share coupons, exclusive offers, promo codes, or cards and codes using any app.
- Sync with supported Gadgetbridge or Wear OS smartwatches.
- Dark theme and accessibility options for vision impaired users.
- Made for everyone by the libre software community.
- Localized hand-made translations for 40+ languages.
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Loaded 100 of 432 files, more files were not shown because too many files have changed in this diff. Show more