Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22193b1872 | ||
|
|
306e7b5201 | ||
|
|
0c8104af96 | ||
|
|
3b06f7a105 | ||
|
|
0869623c1d | ||
|
|
bfa79a6f68 | ||
|
|
cda842485f | ||
|
|
db35306a26 | ||
|
|
2363861a62 | ||
|
|
2960585250 | ||
|
|
ff830ee50d | ||
|
|
3c917da56a | ||
|
|
6ea1d9ec73 | ||
|
|
5ebd7cbb63 | ||
|
|
0d91444ee2 | ||
|
|
9459c78e7c | ||
|
|
b326a7148d | ||
|
|
5ae850dbe8 | ||
|
|
2ef9c9c68e | ||
|
|
613bd000d2 | ||
|
|
f7ea5bcb7b | ||
|
|
b8b5b8ae9b | ||
|
|
e6085a64be | ||
|
|
e77acf9b1e | ||
|
|
dc4299d315 | ||
|
|
1c8d2ba19e | ||
|
|
d46455c021 | ||
|
|
abd4c24138 | ||
|
|
1e922ae074 | ||
|
|
335059a681 | ||
|
|
a530991119 | ||
|
|
45130b5f85 | ||
|
|
70fd411640 | ||
|
|
4d47cd4a4a | ||
|
|
bb694ca9cb | ||
|
|
7ede8424d0 | ||
|
|
1167cf51b5 | ||
|
|
13d2c81c5c | ||
|
|
102a34a3f3 | ||
|
|
0c490f2bdb | ||
|
|
8f69f9cef9 | ||
|
|
00e65a39de | ||
|
|
8698c6099c | ||
|
|
da3fbdb401 | ||
|
|
b28052c46c | ||
|
|
b438a7f03d | ||
|
|
c144035d95 | ||
|
|
7658c3980a | ||
|
|
c11297a733 | ||
|
|
c89e60adf6 | ||
|
|
fa33f61183 | ||
|
|
f3e57d816c | ||
|
|
bf2e1eface | ||
|
|
a69fff9b53 | ||
|
|
9fc0038bd5 | ||
|
|
ffd8dd5633 | ||
|
|
c3947b0f6a | ||
|
|
84b17b17fe | ||
|
|
3956a1123e | ||
|
|
f04185f6fd | ||
|
|
10d188ee40 | ||
|
|
63e2d3740f | ||
|
|
672173437f | ||
|
|
2c851158f5 | ||
|
|
367733676a | ||
|
|
538cd2078b | ||
|
|
731833d9be | ||
|
|
7ebd71283f | ||
|
|
5eb1942ad2 | ||
|
|
999222f3a4 | ||
|
|
e82d726898 | ||
|
|
0f48c75512 | ||
|
|
183d3ea666 | ||
|
|
4e4e9123f2 | ||
|
|
c09566d38a | ||
|
|
750ac6f2ec | ||
|
|
1ed78fa680 | ||
|
|
48a26cf1f7 | ||
|
|
bc6a63a1f0 | ||
|
|
45f360904f | ||
|
|
bdf39a1dd7 | ||
|
|
abff198d01 | ||
|
|
6721ecf113 | ||
|
|
3899c71317 | ||
|
|
b8e4ec9c9f | ||
|
|
92cb120dd0 | ||
|
|
7a8f9c38eb | ||
|
|
dc7498e505 | ||
|
|
02e925f77a | ||
|
|
7dc2a479b6 | ||
|
|
1913d629e7 | ||
|
|
59a750f41c | ||
|
|
46dfafe0b2 | ||
|
|
1faab05032 | ||
|
|
c895f0db56 | ||
|
|
c622df96f0 | ||
|
|
30ac13ec73 | ||
|
|
efbe809982 | ||
|
|
92df5dd36a | ||
|
|
ebdf13678a | ||
|
|
0a0d46033c | ||
|
|
7f72565e24 | ||
|
|
cfcf1e2fcb | ||
|
|
faf2f7a38c | ||
|
|
ace7e5abdd | ||
|
|
f50e547980 | ||
|
|
c05a303568 | ||
|
|
4fcd41d3cc | ||
|
|
3e6c4c1b13 | ||
|
|
55d356ea40 | ||
|
|
a752421efe | ||
|
|
aa4b41612f | ||
|
|
f576228c2d | ||
|
|
c93add3b83 | ||
|
|
117446cebb | ||
|
|
4bee5ec6c8 | ||
|
|
92776c9e8d | ||
|
|
eea3fb5c54 | ||
|
|
dbd0727af2 | ||
|
|
a4cf49754a | ||
|
|
587c5991f1 | ||
|
|
10234ffcef | ||
|
|
2a2710e391 | ||
|
|
f1b5423d94 | ||
|
|
1cb916d16a | ||
|
|
6396120fb0 | ||
|
|
2fe5157519 | ||
|
|
4ce480defb | ||
|
|
3ea5d16f19 | ||
|
|
31fc58576c | ||
|
|
9573799eba | ||
|
|
df94e94b31 | ||
|
|
98b561b186 | ||
|
|
8ee469343c | ||
|
|
e54d129577 | ||
|
|
a7a3237f4f |
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "<xliff" app/src/main/res/values*/strings.xml; then echo "Invalidly escaped translations found"; exit 1; fi
|
||||
- name: Check app_name consistency
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 790 B |
|
After Width: | Height: | Size: 752 B |
|
After Width: | Height: | Size: 499 B |
|
After Width: | Height: | Size: 509 B |
|
After Width: | Height: | Size: 478 B |
|
After Width: | Height: | Size: 346 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 919 B |
|
After Width: | Height: | Size: 656 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
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" />
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,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 © 2016–2020 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 8.9 KiB |