Compare commits

..
Author SHA1 Message Date
Sylvia van Os 0b2dec3b88 Merge branch 'main' into feature/android_17_target 2026-07-15 17:20:37 +02:00
Sylvia van Os 6d79a2e20d Migrate to sdkSpec 2026-07-15 17:20:01 +02:00
Sylvia van Os 0a12429b41 Target Android 17 2026-06-10 21:25:55 +02:00
432 changed files with 205 additions and 3925 deletions

No files matched your search

+16 -27
View File
@@ -4,7 +4,11 @@ on:
push:
branches:
- main
- staging
- trying
pull_request:
branches:
- main
permissions:
actions: none
checks: none
@@ -26,13 +30,9 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
flavor: [fdroidLegacy, foss, gplay]
flavor: [Foss, Gplay]
steps:
- uses: actions/checkout@v7.0.1
- name: Ensure sentence-cased flavor variables
run: |
flavor="$(echo ${{ matrix.flavor }})"
echo "sentenceCasedFlavor=${flavor^}" >> "$GITHUB_ENV"
- uses: actions/checkout@v7.0.0
- uses: gradle/actions/wrapper-validation@v6.2.0
- name: set up OpenJDK 25
run: |
@@ -40,46 +40,35 @@ jobs:
sudo apt-get install -y openjdk-25-jdk-headless
sudo update-alternatives --auto java
- name: Build
run: ./gradlew assemble${sentenceCasedFlavor}Release
run: ./gradlew assemble${{ matrix.flavor }}Release
- 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 "$(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
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:]')"
- name: Check lint
run: ./gradlew lint${sentenceCasedFlavor}Release
run: ./gradlew lint${{ matrix.flavor }}Release
- name: Run unit tests
run: timeout 5m ./gradlew test${sentenceCasedFlavor}DebugUnitTest || { ./gradlew --stop && timeout 5m ./gradlew test${sentenceCasedFlavor}DebugUnitTest; }
run: timeout 5m ./gradlew test${{ matrix.flavor }}DebugUnitTest || { ./gradlew --stop && timeout 5m ./gradlew test${{ matrix.flavor }}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.38.0
uses: ReactiveCircus/android-emulator-runner@v2.37.0
with:
api-level: 23
arch: x86_64
script: ./gradlew connected${sentenceCasedFlavor}DebugAndroidTest
script: ./gradlew connected${{ matrix.flavor }}DebugAndroidTest
- name: Run instrumented tests (API 35)
uses: ReactiveCircus/android-emulator-runner@v2.38.0
uses: ReactiveCircus/android-emulator-runner@v2.37.0
with:
api-level: 35
arch: x86_64
script: ./gradlew connected${sentenceCasedFlavor}DebugAndroidTest
script: ./gradlew connected${{ matrix.flavor }}DebugAndroidTest
- name: Archive test results
if: always()
uses: actions/upload-artifact@v7.0.1
with:
name: test-results-flavor-${{ matrix.flavor }}
name: test-results-flavor${{ matrix.flavor }}
path: app/build/reports
+2 -2
View File
@@ -19,9 +19,9 @@ jobs:
steps:
- name: Checkout repo
id: checkout
uses: actions/checkout@v7.0.1
uses: actions/checkout@v7.0.0
- name: Setup Python
uses: actions/setup-python@v7.0.0
uses: actions/setup-python@v6.3.0
with:
python-version: '3.x'
- name: Run converter script
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
steps:
- name: Checkout repo
id: checkout
uses: actions/checkout@v7.0.1
uses: actions/checkout@v7.0.0
- 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.1
- uses: actions/checkout@v7.0.0
- name: Install requirements
run: |
sudo apt-get update
+5 -1
View File
@@ -4,7 +4,11 @@ on:
push:
branches:
- main
- staging
- trying
pull_request:
branches:
- main
permissions:
actions: none
checks: none
@@ -23,7 +27,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@v7.0.0
- 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
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
update-locales:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@v7.0.0
- name: Add new locales
run: .scripts/new-locales.py
- name: Update locales
-53
View File
@@ -1,53 +0,0 @@
name: Android Wear CI
on:
workflow_dispatch:
push:
branches:
- main
pull_request:
permissions:
actions: none
checks: none
contents: read
deployments: none
discussions: none
id-token: none
issues: none
packages: none
pages: none
pull-requests: none
repository-projects: none
security-events: none
statuses: none
env:
JAVA_HOME: /usr/lib/jvm/java-25-openjdk-amd64
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.0
- uses: gradle/actions/wrapper-validation@v6.2.0
- name: set up OpenJDK 25
run: |
sudo apt-get update
sudo apt-get install -y openjdk-25-jdk-headless
sudo update-alternatives --auto java
- name: Build
run: ./gradlew :wear:assembleRelease
- name: Check for non-free libraries
run: |
# Get Katastima APK scanner
wget -q https://codeberg.org/Katastima/apkscanner/releases/download/v0.0.9/apk-scanner_v0.0.9.jar
# Print pretty result
java -jar apk-scanner_v0.0.9.jar scan-apk "wear/build/outputs/apk/release/wear-release-unsigned.apk"
# Check for offending libraries
# Exit code 4 = no result, which is what we want
set +e
java -jar apk-scanner_v0.0.9.jar scan-apk --json=yes "wear/build/outputs/apk/release/wear-release-unsigned.apk" | jq --exit-status '.libraryCheckResult.offendingLibraries[]'
if [ "$?" -ne 4 ]; then
echo "Bad libraries found!"
exit 1
fi
set -e
- name: Check lint
run: ./gradlew :wear:lintRelease
-3
View File
@@ -19,8 +19,6 @@
/app/*.log
/app/build
/app/release
/wear/build
/wear/release
/.idea/*
!/.idea/icon.svg
# Bundle
@@ -30,4 +28,3 @@
# Catima-specific
SHA256SUMS
/shared/build
+1 -1
View File
@@ -15,7 +15,7 @@ with open('CHANGELOG.md') as changelog:
changelogs[version_code] = text
text = []
match = re.match("## .+ - (\d*).*", line)
match = re.match("## \S* - (\d*).*", line)
if not match:
raise ValueError(f"Invalid version line: {line}")
version_code = match.group(1)
-13
View File
@@ -1,18 +1,5 @@
# 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 -2
View File
@@ -1,12 +1,11 @@
**Last updated**
August 7 2026
April 29 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 -14
View File
@@ -13,14 +13,20 @@ kotlin {
android {
namespace = "protect.card_locker"
compileSdk = 36
compileSdk {
version = release(37)
}
defaultConfig {
applicationId = "me.hackerchick.catima"
minSdk = 23
targetSdk = 36
versionCode = 1002
versionName = "2.45.0"
minSdk {
version = release(23)
}
targetSdk {
version = release(37)
}
versionCode = 167
versionName = "2.43.0"
vectorDrawables.useSupportLibrary = true
multiDexEnabled = true
@@ -59,14 +65,8 @@ 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,8 +119,6 @@ android {
}
dependencies {
implementation(project(":shared"))
// AndroidX
implementation(libs.androidx.appcompat.appcompat)
implementation(libs.androidx.constraintlayout.constraintlayout)
@@ -173,6 +171,7 @@ 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"))
@@ -180,7 +179,7 @@ tasks.register("copyRawResFiles", Copy::class) {
}.also {
tasks.preBuild.dependsOn(it)
tasks.getByName<Delete>("clean") {
val filesNamesToDelete = listOf("PRIVACY")
val filesNamesToDelete = listOf("CHANGELOG", "PRIVACY")
filesNamesToDelete.forEach { fileName ->
delete(layout.projectDirectory.file("src/main/res/raw/${fileName.lowercase()}.md"))
}
-27
View File
@@ -13,12 +13,6 @@
<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
@@ -232,26 +226,5 @@
<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,7 +69,16 @@ fun AboutScreenContent(
stringResource(R.string.version_history),
content.versionHistory,
modifier = Modifier.testTag("card_version_history"),
onClickUrl = "https://catima.app/changelog/"
onClickUrl = "https://catima.app/changelog/",
onClickDialogText = AnnotatedString.fromHtml(
htmlString = content.historyHtml,
linkStyles = TextLinkStyles(
style = SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colorScheme.primary
)
)
)
)
CatimaAboutSection(
stringResource(R.string.credits),
@@ -62,6 +62,18 @@ 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,6 +240,7 @@ 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,7 +14,6 @@ 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) {
@@ -101,7 +100,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 (ForegroundColorHelper.needsDarkForeground(headerColor)) Color.BLACK else Color.WHITE
val foreground = if (Utils.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,7 +91,6 @@ 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 {
@@ -897,8 +896,8 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
Integer headerColor = viewModel.getLoyaltyCard().headerColor;
if (headerColor != null) {
thumbnail.setOnClickListener(new ChooseCardImage());
thumbnailEditIcon.setBackgroundColor(ForegroundColorHelper.Companion.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
thumbnailEditIcon.setColorFilter(ForegroundColorHelper.Companion.needsDarkForeground(headerColor) ? Color.WHITE : Color.BLACK);
thumbnailEditIcon.setBackgroundColor(Utils.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
thumbnailEditIcon.setColorFilter(Utils.needsDarkForeground(headerColor) ? Color.WHITE : Color.BLACK);
}
onResuming = false;
@@ -920,18 +919,18 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
setLoyaltyCardHeaderColor(headerColor);
thumbnail.setBackgroundColor(ForegroundColorHelper.Companion.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
thumbnail.setBackgroundColor(Utils.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
thumbnailEditIcon.setBackgroundColor(ForegroundColorHelper.Companion.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
thumbnailEditIcon.setColorFilter(ForegroundColorHelper.Companion.needsDarkForeground(headerColor) ? Color.WHITE : Color.BLACK);
thumbnailEditIcon.setBackgroundColor(Utils.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
thumbnailEditIcon.setColorFilter(Utils.needsDarkForeground(headerColor) ? Color.WHITE : Color.BLACK);
} else {
generateIcon(storeFieldEdit.getText().toString().trim());
Integer headerColor = viewModel.getLoyaltyCard().headerColor;
if (headerColor != null) {
thumbnailEditIcon.setBackgroundColor(ForegroundColorHelper.Companion.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
thumbnailEditIcon.setColorFilter(ForegroundColorHelper.Companion.needsDarkForeground(headerColor) ? Color.WHITE : Color.BLACK);
thumbnailEditIcon.setBackgroundColor(Utils.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
thumbnailEditIcon.setColorFilter(Utils.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,7 +11,6 @@ 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 {
@@ -42,10 +41,5 @@ 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,7 +37,6 @@ 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
@@ -62,7 +61,6 @@ 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 {
@@ -320,8 +318,6 @@ class MainActivity : CatimaAppCompatActivity(), CardAdapterListener {
}
}
mWearSyncPermissionRequester = WearSyncPermissionRequester(this, this)
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (mSearchView != null && !mSearchView!!.isIconified) {
@@ -398,8 +394,6 @@ class MainActivity : CatimaAppCompatActivity(), CardAdapterListener {
val settings = Settings(this)
layoutManager.setSpanCount(settings.getPreferredColumnCount())
}
mWearSyncPermissionRequester.synchronize()
}
private fun displayCardSetupOptions(menu: Menu, shouldShow: Boolean) {
@@ -473,7 +467,11 @@ class MainActivity : CatimaAppCompatActivity(), CardAdapterListener {
ShortcutHelper.updateShortcuts(mAdapter.mContext)
}
private fun processParseResultList(parseResultList: MutableList<ParseResult?>) {
private fun processParseResultList(
parseResultList: MutableList<ParseResult?>,
group: String?,
closeAppOnNoBarcode: Boolean
) {
require(!parseResultList.isEmpty()) { "parseResultList may not be empty" }
Utils.makeUserChooseParseResultFromList(
@@ -484,12 +482,17 @@ 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() {
finish()
if (closeAppOnNoBarcode) {
finish()
}
}
})
}
@@ -553,7 +556,7 @@ class MainActivity : CatimaAppCompatActivity(), CardAdapterListener {
return
}
processParseResultList(parseResultList)
processParseResultList(parseResultList, null, true)
}
private fun extractIntentFields(intent: Intent) {
@@ -1,11 +0,0 @@
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,7 +18,6 @@ import org.jetbrains.annotations.NotNull;
import java.util.LinkedList;
import protect.card_locker.cardview.LoyaltyCardViewActivity;
import protect.card_locker.shared.ForegroundColorHelper;
public class ShortcutHelper {
/**
@@ -103,7 +102,7 @@ public class ShortcutHelper {
if (iconBitmap == null) {
iconBitmap = Utils.generateIcon(context, loyaltyCard, true).getLetterTile();
} else {
iconBitmap = createAdaptiveBitmap(iconBitmap, ForegroundColorHelper.Companion.needsDarkForeground(Utils.getHeaderColor(context, loyaltyCard)) ? Color.BLACK : Color.WHITE);
iconBitmap = createAdaptiveBitmap(iconBitmap, Utils.needsDarkForeground(Utils.getHeaderColor(context, loyaltyCard)) ? Color.BLACK : Color.WHITE);
}
IconCompat icon = IconCompat.createWithAdaptiveBitmap(iconBitmap);
@@ -39,6 +39,7 @@ 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;
@@ -94,7 +95,6 @@ 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,6 +115,8 @@ 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;
@@ -145,7 +147,11 @@ public class Utils {
}
return new LetterBitmap(context, store, store,
tileLetterFontSize, pixelSize, pixelSize, backgroundColor, ForegroundColorHelper.Companion.needsDarkForeground(backgroundColor) ? Color.BLACK : Color.WHITE);
tileLetterFontSize, pixelSize, pixelSize, backgroundColor, needsDarkForeground(backgroundColor) ? Color.BLACK : Color.WHITE);
}
static public boolean needsDarkForeground(Integer backgroundColor) {
return ColorUtils.calculateLuminance(backgroundColor) > LUMINANCE_MIDPOINT;
}
static public List<ParseResult> retrieveBarcodesFromImage(Context context, Uri uri) {
@@ -1064,7 +1070,7 @@ public class Utils {
if (icon != null) {
// Use header colour to decide if this image will need a white or black background
backgroundOrIcon.setBackgroundColor(ForegroundColorHelper.Companion.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
backgroundOrIcon.setBackgroundColor(needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
// Ensure correct cropping style
backgroundOrIcon.setScaleType(Utils.getRecommendedScaleTypeForThumbnailImage(icon));
@@ -1094,7 +1100,7 @@ public class Utils {
// Actually set the text and colour
textWhenNoImage.setVisibility(View.VISIBLE);
textWhenNoImage.setText(loyaltyCard.store);
textWhenNoImage.setTextColor(ForegroundColorHelper.Companion.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
textWhenNoImage.setTextColor(Utils.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
}
return headerColor;
@@ -2,7 +2,6 @@ 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
@@ -18,15 +17,8 @@ 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
@@ -40,11 +32,9 @@ 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
@@ -78,8 +68,6 @@ class LoyaltyCardImageViewActivity : CatimaComponentActivity() {
CatimaTheme {
LoyaltyCardImageViewScreen(
bitmap = bitmap,
imageLocationType = imageLocationType,
cardId = loyaltyCardId,
contentDescriptionRes = imageLocationType.descriptionRes(),
onBackPressedDispatcher = onBackPressedDispatcher
)
@@ -136,12 +124,9 @@ 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) }
@@ -159,24 +144,7 @@ fun LoyaltyCardImageViewScreen(
topBar = {
CatimaTopAppBar(
title = stringResource(contentDescriptionRes),
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)) },
)
}
}
onBackPressedDispatcher = onBackPressedDispatcher
)
}
) { innerPadding ->
@@ -228,13 +196,4 @@ private fun Offset.clampTo(size: IntSize, scale: Float): Offset {
)
}
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
private const val MAX_IMAGE_SCALE = 5F
@@ -54,7 +54,6 @@ 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;
@@ -599,20 +598,20 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
// Set bottomAppBar and system navigation bar color
binding.bottomAppBar.setBackgroundColor(darkenedColor);
Utils.setNavigationBarColor(null, window, darkenedColor, ForegroundColorHelper.Companion.needsDarkForeground(darkenedColor));
Utils.setNavigationBarColor(null, window, darkenedColor, Utils.needsDarkForeground(darkenedColor));
int complementaryColor = Utils.getComplementaryColor(darkenedColor);
binding.fabEdit.setBackgroundTintList(ColorStateList.valueOf(complementaryColor));
Drawable editButtonIcon = binding.fabEdit.getDrawable();
editButtonIcon.mutate();
editButtonIcon.setTint(ForegroundColorHelper.Companion.needsDarkForeground(complementaryColor) ? Color.BLACK : Color.WHITE);
editButtonIcon.setTint(Utils.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 = ForegroundColorHelper.Companion.needsDarkForeground(backgroundHeaderColor);
backgroundNeedsDarkIcons = Utils.needsDarkForeground(backgroundHeaderColor);
fixBottomAppBarImageButtonColor(binding.bottomAppBarInfoButton);
fixBottomAppBarImageButtonColor(binding.bottomAppBarPreviousButton);
@@ -3,7 +3,6 @@ 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
@@ -23,9 +22,7 @@ import protect.card_locker.preferences.Settings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CatimaTopAppBar(title: String,
onBackPressedDispatcher: OnBackPressedDispatcher?,
actions : @Composable RowScope.() -> Unit = {}) {
fun CatimaTopAppBar(title: String, onBackPressedDispatcher: OnBackPressedDispatcher?) {
// Use pure black in OLED theme
val context = LocalContext.current
val settings = Settings(context)
@@ -54,7 +51,6 @@ fun CatimaTopAppBar(title: String,
)
}
}
},
actions = actions
}
)
}
@@ -112,8 +112,4 @@ 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,42 +1,22 @@
package protect.card_locker.preferences
import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.content.BroadcastReceiver
import android.content.Context
import android.app.Activity
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() {
@@ -101,62 +81,11 @@ 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)
setupThemePreference()
setupOledDarkPreference()
setupLocalePreference()
setupCrashReporterPreference()
setupWearSyncPreference()
setupWearSyncDevicePreferences()
}
private fun setupThemePreference() {
// Show pretty names and summaries
val themePreference = findPreference<ListPreference>(getString(R.string.settings_key_theme))
themePreference!!.setOnPreferenceChangeListener { _, o ->
when (o.toString()) {
@@ -172,17 +101,13 @@ class SettingsActivity : CatimaAppCompatActivity() {
}
true
}
}
private fun setupOledDarkPreference() {
val oledDarkPreference = findPreference<Preference>(getString(R.string.settings_key_oled_dark))
oledDarkPreference!!.setOnPreferenceChangeListener { _, _ ->
refreshActivity()
refreshActivity(true)
true
}
}
private fun setupLocalePreference() {
val localePreference =
findPreference<ListPreference>(getString(R.string.settings_key_locale))!!
localePreference.let {
@@ -223,7 +148,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()
refreshActivity(true)
return@setOnPreferenceChangeListener true
}
val newLocale = newValue as String
@@ -231,205 +156,14 @@ 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 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
private fun refreshActivity(reloadMain: Boolean) {
mReloadMain = reloadMain || mReloadMain
activity?.recreate()
}
}
@@ -1,68 +0,0 @@
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()
}
}
@@ -1,8 +0,0 @@
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"
}
@@ -1,122 +0,0 @@
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()
}
@@ -1,33 +0,0 @@
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"
}
}
@@ -1,310 +0,0 @@
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) { }
}
}
}
@@ -1,167 +0,0 @@
package protect.card_locker.wearos
import android.Manifest
import android.app.ForegroundServiceStartNotAllowedException
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.activity.result.ActivityResultCaller
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import protect.card_locker.NotificationInfo
import protect.card_locker.R
import protect.card_locker.preferences.Settings
import protect.card_locker.shared.BluetoothPermissionHelper
class WearSyncPermissionRequester(
caller: ActivityResultCaller,
context: Context
) {
private val context = context.applicationContext
private var pendingResultCallback: ((Boolean) -> Unit)? = null
private val permissionLauncher = caller.registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { results ->
val allGranted = results.all { it.value }
pendingResultCallback?.invoke(allGranted)
WearSyncServiceManager.onPermissionResult(this.context, allGranted)
pendingResultCallback = null
}
fun synchronize() {
WearSyncServiceManager.synchronize(context) {
permissionLauncher.launch(getRequiredPermissions())
}
}
fun onWearSyncChanged(enabled: Boolean, onPermissionResult: ((Boolean) -> Unit)? = null) {
val applied = WearSyncServiceManager.onWearSyncChanged(context, enabled) {
pendingResultCallback = onPermissionResult
permissionLauncher.launch(getRequiredPermissions())
}
if (applied) {
onPermissionResult?.invoke(true)
}
}
private fun getRequiredPermissions(): Array<String> = buildList {
if (BluetoothPermissionHelper.isBluetoothConnectRequired()) {
add(Manifest.permission.BLUETOOTH_CONNECT)
}
if (BluetoothPermissionHelper.isPostNotificationsRequired()) {
add(Manifest.permission.POST_NOTIFICATIONS)
}
}.toTypedArray()
}
object WearSyncServiceManager {
private const val TAG = "CatimaWearSyncServiceManager"
fun synchronize(context: Context, requestPermission: (() -> Unit)? = null) {
if (!Settings(context).wearSyncEnabled) {
stop(context)
} else if (arePermissionsGranted(context)) {
start(context)
} else if (requestPermission != null) {
requestPermission()
} else {
stop(context)
}
}
fun onWearSyncChanged(
context: Context,
enabled: Boolean,
requestPermission: () -> Unit
): Boolean {
return if (enabled) {
if (arePermissionsGranted(context)) {
start(context)
true
} else {
requestPermission()
false
}
} else {
stop(context)
true
}
}
private fun arePermissionsGranted(context: Context): Boolean {
return BluetoothPermissionHelper.isBluetoothConnectGranted(context) &&
BluetoothPermissionHelper.isPostNotificationsGranted(context)
}
fun onPermissionResult(context: Context, granted: Boolean) {
if (granted && Settings(context).wearSyncEnabled) {
start(context)
} else {
stop(context)
if (!granted) {
PreferenceManager.getDefaultSharedPreferences(context).edit {
putBoolean(context.getString(R.string.settings_key_wear_sync), false)
}
}
}
}
private fun start(context: Context) {
// Exception can only throw on API level 31 and up
if (Build.VERSION.SDK_INT >= 31) {
try {
ContextCompat.startForegroundService(
context,
Intent(context, BluetoothServerService::class.java)
)
} catch (e: ForegroundServiceStartNotAllowedException) {
// We naively assume we have enough permissions to show a notification as the service should never try to start
// before the user gave notification access anyway
Log.d(TAG, "Foreground service failed to launch: $e")
val channel = NotificationChannel(
NotificationInfo.WearBluetooth.CRITICAL_ERROR_CHANNEL_ID,
context.getString(R.string.wear_bt_critical_error_channel_name),
NotificationManager.IMPORTANCE_HIGH
).apply { setShowBadge(false) }
context.getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
val notification = NotificationCompat.Builder(context, NotificationInfo.WearBluetooth.CRITICAL_ERROR_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification_error)
.setContentTitle(context.getString(R.string.wear_bt_failed_foreground_notification_title))
.setContentText(e.message)
.build()
with(NotificationManagerCompat.from(context)) {
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
return@with
}
notify(
NotificationInfo.WearBluetooth.CRITICAL_ERROR_NOTIFICATION_ID,
notification
)
}
}
} else {
ContextCompat.startForegroundService(
context,
Intent(context, BluetoothServerService::class.java)
)
}
}
private fun stop(context: Context) {
context.stopService(Intent(context, BluetoothServerService::class.java))
}
}
File diff suppressed because one or more lines are too long.
@@ -1,30 +0,0 @@
<!--
~ 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>
@@ -1,30 +0,0 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="#FFFFFF">
<group android:scaleX="0.92"
android:scaleY="0.92"
android:translateX="38.4"
android:translateY="38.4">
<path
android:fillColor="@android:color/white"
android:pathData="M160,800L160,720L270,720L254,706Q202,660 181,601Q160,542 160,482Q160,371 226.5,284.5Q293,198 400,170L400,254Q328,280 284,342.5Q240,405 240,482Q240,527 257,569.5Q274,612 310,648L320,658L320,560L400,560L400,800L160,800ZM560,790L560,706Q632,680 676,617.5Q720,555 720,478Q720,433 703,390.5Q686,348 650,312L640,302L640,400L560,400L560,160L800,160L800,240L690,240L706,254Q755,303 777.5,360.5Q800,418 800,478Q800,589 733.5,675.5Q667,762 560,790Z"/>
</group>
</vector>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 752 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 499 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 478 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 919 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 656 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 918 B

@@ -1,22 +0,0 @@
<?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>
@@ -1,13 +0,0 @@
<?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" />
+29 -34
View File
@@ -3,11 +3,11 @@ Branden Archer
J. Lavoie
solokot
Allan Nordhøy
Oğuz Ersen
Heimen Stoffels
B o d o
Oğuz Ersen
FC (Fay) Stegerman
大王叫我来巡山
B o d o
StoyanDimitrov
SlavekB
Katharine Chui / Katie
@@ -17,32 +17,31 @@ IllusiveMan196
Максим Горпиніч
Edgars Andersons
Liner Seven
Fjuro
GitSpoon
Joel A
Altonss
Priit Jõerüüt
Fjuro
Joel A
GitSpoon
Michael Moroni
Priit Jõerüüt
Eric
GM
Petr Novák
laralem
Vasilis
Petr Novák
Taco
Milo Ivir
nadiafekihahmed
pfaffenrodt
Vasilis
Aayush Gupta
damjang
Scrambled777
josé m
ikanakova
Nyatsuki
Jiri Grönroos
Giovanni Donisi
Kachelkaiser
109247019824
Jiri Grönroos
HudobniVolk
109247019824
Warder
Balázs Meskó
Traductor
@@ -56,11 +55,11 @@ Feike
Arno-github
Jose Delvani
Milan Šalka
damjang
Robin
தமிழ்நேரம்
huuhaa
Skrripy
NooB9496
Govindgopalyadav
Gideon
waffshappen
@@ -69,43 +68,39 @@ JungHee Lee
pacavarn
ngocanhtve
aradxxx
Watchman89
StellarSand
Quentin PAGÈS
Projjal Moitra
Aliaksandr Trush
e-michalak
தமிழ் நேரம்
josé m.
hajertabbane
inavleb
Ziad OUALHADJ
Robin Liu
Ricky Tigg
Renko
Denis Shilin
Renko
Ricky Tigg
Robin Liu
Ziad OUALHADJ
inavleb
しいたけ
Alexander Ivanov
Thu-Trang Pham
Tim Trek
Virginie
dukezas
ehrt74
ezn24
mrestivill
stavpup
Miha Frangež
stavpup
mrestivill
ezn24
ehrt74
Virginie
Tim Trek
Thu-Trang Pham
Alì Mortacci
Andre Costa
arshbeerSingh
Augustin LAVILLE
Freddo espresso
Govind S Nair
Kim Seohyun
Libre
echo r"0xX4H" | rev
rudy3
Michael Gangolf
PRATHAMESH BHAGAT
Peter Dave Hello
PRATHAMESH BHAGAT
Michael Gangolf
rudy3
echo r"0xX4H" | rev
Kim Seohyun
Govind S Nair
Thomas Bertels
+4 -4
View File
@@ -16,7 +16,7 @@
<string name="edit">تعديل</string>
<string name="delete">حذف</string>
<string name="confirm">تأكيد</string>
<string name="deleteConfirmation">هل ترغب في حذف هذه البطاقة نهائيًّا؟</string>
<string name="deleteConfirmation">حذف هذه البطاقة نهائيًّا؟</string>
<string name="ok">حسنًا</string>
<string name="share">مشاركة</string>
<string name="sendLabel">إرسال…</string>
@@ -161,9 +161,9 @@
<item quantity="zero">حذف هذه <xliff:g>%d</xliff:g> البطاقة نهائيًا؟</item>
<item quantity="one">حذف هذه <xliff:g>%d</xliff:g> البطاقة نهائيًا؟</item>
<item quantity="two">حذف هذه <xliff:g>%d</xliff:g> البطاقتين نهائيًا؟</item>
<item quantity="few">هل ترغب في حذف هذه البطاقات الـ <xliff:g>%d</xliff:g> نهائيًّا؟</item>
<item quantity="many">هل ترغب في حذف هذه البطاقات الـ <xliff:g>%d</xliff:g> نهائيًّا؟</item>
<item quantity="other">هل ترغب في حذف هذه البطاقات الـ <xliff:g>%d</xliff:g> نهائيًا؟</item>
<item quantity="few">حذف هذه <xliff:g>%d</xliff:g> البطاقات نهائيًا؟</item>
<item quantity="many">حذف هذه <xliff:g>%d</xliff:g> البطاقات نهائيًا؟</item>
<item quantity="other">حذف هذه <xliff:g>%d</xliff:g> البطاقات نهائيًا؟</item>
</plurals>
<string name="importOptionFilesystemTitle">الاستيراد من نظام الملفات</string>
<string name="importOptionFilesystemExplanation">اختر ملفًا محددًا من نظام الملفات</string>
-5
View File
@@ -303,9 +303,4 @@
<string name="nfc_blocked_while_viewing_card">NFC е на пауза</string>
<string name="change_settings">Настройки</string>
<string name="nfc_block_system_error">Грешка при спиране на NFC</string>
<string name="sort_by_most_recently_added">Поредност на добавяне</string>
<string name="zoomFrontImage">Увеличаване на предното изображение</string>
<string name="zoomBackImage">Увеличаване на задното изображение</string>
<string name="show_card_id">Показване номера на картата</string>
<string name="open_in_gallery">Отваряне в галерията</string>
</resources>
+5 -5
View File
@@ -3,13 +3,13 @@
<string name="save">সংরক্ষণ</string>
<string name="cancel">বাতিল</string>
<string name="unstar">তারা মুক্ত</string>
<string name="star">পছন্দের তালিকায় যোগ করুন</string>
<string name="barcodeType">বারকোডের ধরন</string>
<string name="note">উল্লেখ্য</string>
<string name="storeName">নাম</string>
<string name="star">তারা</string>
<string name="barcodeType">বারকোড ধরন</string>
<string name="note">বিঃদ্রঃ</string>
<string name="storeName">দোকানের নাম</string>
<string name="noMatchingGiftCards">কোনো ফলাফল পাওয়া যায়নি। অনুসন্ধানের বাক্যটি বদলে দেখুন।</string>
<string name="noGiftCards">প্লাস বোতামটি টিপে একটি কার্ড যোগ করুন বা ⋮ মেনু থেকে কিছু নিয়ে আসুন।</string>
<string name="action_add">যোগ করুন</string>
<string name="action_add">কর্ম যোগ</string>
<string name="all">সব</string>
<string name="noGroupCards">এই গ্রুপটি খালি</string>
<string name="noGroups">+ যোগ বোতামটি টিপে গ্রুপ যোগ করুন যাতে তাদের শ্রেণীকরণ করা যায়।</string>
-5
View File
@@ -309,9 +309,4 @@
<string name="nfc_blocked_while_viewing_card">NFC je pozastaveno</string>
<string name="change_settings">Změnit nastavení</string>
<string name="nfc_block_system_error">Nepodařilo se pozastavit NFC</string>
<string name="sort_by_most_recently_added">Nedávno přidáno</string>
<string name="zoomFrontImage">Přiblížit přední obrázek</string>
<string name="zoomBackImage">Přiblížit zadní obrázek</string>
<string name="show_card_id">Zobrazit ID karty</string>
<string name="open_in_gallery">Otevřít v galerii</string>
</resources>
-5
View File
@@ -303,9 +303,4 @@
<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,9 +303,4 @@
<string name="nfc_blocked_while_viewing_card">Το NFC είναι σε παύση</string>
<string name="change_settings">Αλλαγή ρυθμίσεων</string>
<string name="nfc_block_system_error">Αποτυχία παύσης NFC</string>
<string name="sort_by_most_recently_added">Πρόσφατα προστεθέντα</string>
<string name="zoomFrontImage">Μεγέθυνση μπροστινής εικόνας</string>
<string name="zoomBackImage">Μεγέθυνση πίσω εικόνας</string>
<string name="show_card_id">Εμφάνιση ταυτότητας κάρτας</string>
<string name="open_in_gallery">Άνοιγμα στη συλλογή εικόνων</string>
</resources>
-4
View File
@@ -309,8 +309,4 @@
<string name="nfc_blocked_while_viewing_card">NFC está pausado</string>
<string name="change_settings">Cambiar ajustes</string>
<string name="nfc_block_system_error">Incorrecto al pausar NFC</string>
<string name="sort_by_most_recently_added">Más recientemente añadido</string>
<string name="zoomFrontImage">Imagen frontal de zoom</string>
<string name="zoomBackImage">Imagen trasera de zoom</string>
<string name="show_card_id">Mostrar ID de tarjeta</string>
</resources>
-5
View File
@@ -303,9 +303,4 @@
<string name="change_settings">Muuda seadistusi</string>
<string name="settings_disable_nfc_while_viewing_card_summary">Vältimaks, et lähiväljaside (NFC) abil toimiv makseliides käivitub, lülitatakse kaardi vaatamisel lähiväljaside (NFC) välja</string>
<string name="nfc_block_system_error">Lähiväljaside (NFC) peatamine ei õnnestunud</string>
<string name="sort_by_most_recently_added">Viimasena lisatud</string>
<string name="zoomFrontImage">Suumi esikaane pilti</string>
<string name="zoomBackImage">Suumi tagakaane pilti</string>
<string name="show_card_id">Näita kaardi tunnust</string>
<string name="open_in_gallery">Ava pildigaleriis</string>
</resources>
-10
View File
@@ -299,14 +299,4 @@
<string name="back">Takaisin</string>
<string name="acra_explain_crash">Jos mahdollista lisää tietoja toiminnastasi täällä:</string>
<string name="acra_crash_email_subject"><xliff:g id="app_name">%s</xliff:g> pysähtymisraportti</string>
<string name="settings_disable_nfc_while_viewing_card">Keskeytä NFC</string>
<string name="settings_disable_nfc_while_viewing_card_summary">Keskeyttää NFC:n, jotta NFC-maksut eivät aktivoituisi korttia katsoessa</string>
<string name="sort_by_most_recently_added">Viimeksi lisätty</string>
<string name="zoomFrontImage">Suurenna etukuvaa</string>
<string name="zoomBackImage">Suurenna takakuvaa</string>
<string name="show_card_id">Näytä kortin tunniste</string>
<string name="nfc_blocked_while_viewing_card">NFC on keskeytetty</string>
<string name="change_settings">Muuta asetuksia</string>
<string name="nfc_block_system_error">NFC:n keskeyttäminen epäonnistui</string>
<string name="open_in_gallery">Avaa kuvagalleriassa</string>
</resources>
-5
View File
@@ -309,9 +309,4 @@
<string name="nfc_blocked_while_viewing_card">Le NFC est en pause</string>
<string name="change_settings">Modifier les paramètres</string>
<string name="nfc_block_system_error">Erreur lors de la mise en pause du NFC</string>
<string name="sort_by_most_recently_added">Récemment ajoutés</string>
<string name="zoomFrontImage">Grossir l\'image avant</string>
<string name="zoomBackImage">Grossir l\'image arrière</string>
<string name="show_card_id">Afficher l\'ID de carte</string>
<string name="open_in_gallery">Ouvrir dans la galerie d\'images</string>
</resources>
-5
View File
@@ -302,9 +302,4 @@
<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,8 +297,4 @@
<string name="nfc_blocked_while_viewing_card">NFC dijeda</string>
<string name="change_settings">Ubah pengaturan</string>
<string name="nfc_block_system_error">Gagal menjeda NFC</string>
<string name="show_card_id">Tampilkan ID Kartu</string>
<string name="sort_by_most_recently_added">Terakhir ditambahkan</string>
<string name="zoomFrontImage">Perbesar gambar depan</string>
<string name="zoomBackImage">Perbesar gambar belakang</string>
</resources>
-5
View File
@@ -309,9 +309,4 @@
<string name="nfc_blocked_while_viewing_card">La funzione NFC è disattivata</string>
<string name="change_settings">Modifica impostazioni</string>
<string name="nfc_block_system_error">Impossibile mettere in pausa l\'NFC</string>
<string name="sort_by_most_recently_added">Aggiunte più di recente</string>
<string name="show_card_id">Mostra ID carta</string>
<string name="zoomFrontImage">Zoom immagine fronte</string>
<string name="zoomBackImage">Zoom immagine retro</string>
<string name="open_in_gallery">Apri nella galleria immagini</string>
</resources>
-5
View File
@@ -296,9 +296,4 @@
<string name="nfc_blocked_while_viewing_card">NFCは停止中です</string>
<string name="change_settings">設定を変更する</string>
<string name="nfc_block_system_error">NFCの一時停止に失敗</string>
<string name="sort_by_most_recently_added">最近追加されたもの</string>
<string name="zoomFrontImage">券面を拡大</string>
<string name="zoomBackImage">背面を拡大</string>
<string name="show_card_id">カードIDを表示</string>
<string name="open_in_gallery">ギャラリーを開く</string>
</resources>
+11 -11
View File
@@ -11,10 +11,10 @@
<string name="delete">Ištrinti</string>
<string name="confirm">Patvirtinti</string>
<string name="ok">Gerai</string>
<string name="editCardTitle">Redaguoti kortelę</string>
<string name="addCardTitle">Pridėti kortelę</string>
<string name="editCardTitle">Redaguoti lojalumo kortelę</string>
<string name="addCardTitle">Pridėti lojalumo kortelę</string>
<string name="scanCardBarcode">Nuskaityti brūkšninį kodą</string>
<string name="importExport">Importas/Exportas</string>
<string name="importExport">Importuoti/Exportuoti</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 nuskaityti importo URI</string>
<string name="failedParsingImportUriError">Nepavyko iššifruoti 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,9 +65,11 @@
<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">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="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="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 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, pirmiausia ten paspausdami Eksportuoti.</string>
<string name="importCatima">Importuoti iš Catima</string>
<string name="accept">Priimti</string>
<string name="privacy_policy">Privatumo politika</string>
@@ -115,8 +117,8 @@
<string name="settings">Nustatymai</string>
<string name="starImage">Mėgstamiausia žvaigždė</string>
<string name="thumbnailDescription">Miniatiūra</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_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_copyright_old">Paremta Loyalty Card Keychain
\nautorinės teisės © 20162020 Branden Archer</string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Autorinės teisės © 2019<xliff:g>%d</xliff:g> Sylvia van Os</string>
@@ -246,7 +248,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">Skaityti Catima korteles</string>
<string name="permissionReadCardsLabel">Skenuoti 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>
@@ -279,6 +281,4 @@
<string name="settings_column_count_portrait">Stulpeliai vertikaliame režime</string>
<string name="settings_column_count_landscape">Stulpeliai horizontaliame režime</string>
<string name="settings_automatic_column_count">Automatiškai</string>
<string name="settings_disable_nfc_while_viewing_card">NFC pauzė</string>
<string name="settings_disable_nfc_while_viewing_card_summary">Pristabdo NFC, kad peržiūrint kortelę nebūtų suaktyvinti NFC mokėjimai</string>
</resources>
-5
View File
@@ -309,9 +309,4 @@
<string name="settings_disable_nfc_while_viewing_card_summary">Aptur NFC, lai novērstu NFC maksājumu izsaukšanu kartes apskatīšanas laikā</string>
<string name="nfc_blocked_while_viewing_card">NFC ir apturēts</string>
<string name="change_settings">Mainīt iestatījumus</string>
<string name="open_in_gallery">Atvērt attēlu galerijā</string>
<string name="sort_by_most_recently_added">Visnesenāk pievienotās</string>
<string name="zoomFrontImage">Pietuvināt priekšpuses attēlu</string>
<string name="zoomBackImage">Pietuvināt aizmugures attēlu</string>
<string name="show_card_id">Rādīt kartes Id</string>
</resources>
-5
View File
@@ -303,9 +303,4 @@
<string name="settings_disable_nfc_while_viewing_card_summary">Pauzeert NFC om te voorkomen dat NFC-betalingen activeren tijdens het bekijken van een kaart</string>
<string name="nfc_blocked_while_viewing_card">NFC is gepauzeerd</string>
<string name="nfc_block_system_error">Pauzeren van NFC mislukt</string>
<string name="sort_by_most_recently_added">Op onlangs toegevoegd</string>
<string name="show_card_id">Kaart ID tonen</string>
<string name="zoomFrontImage">Voorzijde inzoomen</string>
<string name="zoomBackImage">Achterzijde inzoomen</string>
<string name="open_in_gallery">Open in afbeeldingsgalerij</string>
</resources>
-5
View File
@@ -315,9 +315,4 @@
<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,9 +309,4 @@
<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,6 +309,4 @@
<string name="nfc_blocked_while_viewing_card">NFC está pausado</string>
<string name="change_settings">Alterar configurações</string>
<string name="nfc_block_system_error">Falha ao pausar o NFC</string>
<string name="zoomFrontImage">Ampliar imagem frontal</string>
<string name="show_card_id">Mostrar ID do cartão</string>
</resources>
-3
View File
@@ -308,7 +308,4 @@
<string name="nfc_blocked_while_viewing_card">NFC está pausado</string>
<string name="change_settings">Alterar configurações</string>
<string name="nfc_block_system_error">Erro ao pausar o NFC</string>
<string name="zoomFrontImage">Ampliar imagem frontal</string>
<string name="show_card_id">Mostrar ID do cartão</string>
<string name="sort_by_most_recently_added">Adicionados mais recentemente</string>
</resources>
-5
View File
@@ -315,9 +315,4 @@
<string name="nfc_blocked_while_viewing_card">NFC приостановлен</string>
<string name="change_settings">Изменить настройки</string>
<string name="nfc_block_system_error">Приостановка NFC невозможна</string>
<string name="sort_by_most_recently_added">Порядок добавления</string>
<string name="show_card_id">Показывать номер карты</string>
<string name="zoomFrontImage">Масштабирование лицевого изображения</string>
<string name="zoomBackImage">Масштабирование заднего изображения</string>
<string name="open_in_gallery">Открыть в галерее</string>
</resources>
-5
View File
@@ -314,9 +314,4 @@
<string name="nfc_blocked_while_viewing_card">NFC je zaustavljen</string>
<string name="change_settings">Spremeni nastavitve</string>
<string name="nfc_block_system_error">ustavitev NFC-ja ni uspela</string>
<string name="sort_by_most_recently_added">Nazadnje dodane</string>
<string name="zoomFrontImage">Povečaj sprednjo sliko</string>
<string name="zoomBackImage">Povečaj zadnjo sliko</string>
<string name="show_card_id">Prikaži ID kartice</string>
<string name="open_in_gallery">Odpri v galeriji slik</string>
</resources>
-5
View File
@@ -303,9 +303,4 @@
<string name="change_settings">Ändra inställningar</string>
<string name="nfc_block_system_error">Lyckades inte pausa NFC</string>
<string name="settings_disable_nfc_while_viewing_card_summary">Pausar NFC för att förhindra att NFC-betalningar genomförs av misstag medan du tittar på ett kort</string>
<string name="sort_by_most_recently_added">Senast tillagda</string>
<string name="zoomFrontImage">Zooma framsidesbilden</string>
<string name="zoomBackImage">Zooma baksidesbilden</string>
<string name="show_card_id">Visa kort-ID</string>
<string name="open_in_gallery">Öppna i bildgalleriet</string>
</resources>
-5
View File
@@ -303,9 +303,4 @@
<string name="nfc_blocked_while_viewing_card">NFC duraklatıldı</string>
<string name="change_settings">Ayarları değiştir</string>
<string name="nfc_block_system_error">NFC duraklatılamadı</string>
<string name="sort_by_most_recently_added">En son eklenen</string>
<string name="zoomFrontImage">Ön görüntüyü yakınlaştır</string>
<string name="zoomBackImage">Arka görüntüyü yakınlaştır</string>
<string name="show_card_id">Kart numarasını göster</string>
<string name="open_in_gallery">Resim galerisinde aç</string>
</resources>
-5
View File
@@ -297,9 +297,4 @@
<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,9 +297,4 @@
<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>
+2 -44
View File
@@ -104,8 +104,8 @@
<string name="settings_disable_nfc_while_viewing_card">Pause NFC</string>
<string name="settings_disable_nfc_while_viewing_card_summary">Pauses NFC to prevent NFC payments from triggering while viewing a card</string>
<string name="settings_key_disable_nfc_while_viewing_card" translatable="false">pref_disable_nfc_while_viewing_card</string>
<string name="settings_allow_content_provider_read_title">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_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_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,42 +304,6 @@
<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>
@@ -389,10 +353,4 @@
<string name="nfc_blocked_while_viewing_card">NFC is paused</string>
<string name="change_settings">Change settings</string>
<string name="nfc_block_system_error">Failed to pause NFC</string>
<string name="open_in_gallery">Open in image gallery</string>
<string name="wear_bt_channel_name">Wear OS companion</string>
<string name="wear_bt_notification_title">Catima Wear OS sync active</string>
<string name="settings_category_title_smartwatch">Smartwatch support</string>
<string name="wear_bt_failed_foreground_notification_title">Failed to start Wear OS sync service</string>
<string name="wear_bt_critical_error_channel_name">Wear OS sync critical error</string>
</resources>
-7
View File
@@ -57,13 +57,6 @@
<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>
+1 -24
View File
@@ -121,7 +121,7 @@
</PreferenceCategory>
<PreferenceCategory
android:title="@string/settings_category_title_smartwatch"
android:title="@string/settings_category_title_privacy"
app:iconSpaceReserved="false">
<SwitchPreferenceCompat
@@ -132,29 +132,6 @@
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,8 +11,6 @@ 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 {
@@ -28,7 +26,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(ForegroundColorHelper.Companion.needsDarkForeground(color));
assertFalse(Utils.needsDarkForeground(color));
}
});
}
@@ -1,37 +0,0 @@
package protect.card_locker.wearos
import android.content.Context
import androidx.preference.PreferenceManager
import androidx.test.core.app.ApplicationProvider
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import protect.card_locker.R
@RunWith(RobolectricTestRunner::class)
class WearSyncServiceManagerTest {
@Test
fun onPermissionResult_denied_disablesWearSync() {
val context = ApplicationProvider.getApplicationContext<Context>()
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
prefs.edit().putBoolean(context.getString(R.string.settings_key_wear_sync), true).commit()
WearSyncServiceManager.onPermissionResult(context, false)
assertFalse(prefs.getBoolean(context.getString(R.string.settings_key_wear_sync), true))
}
@Test
fun onPermissionResult_granted_keepsWearSyncEnabled() {
val context = ApplicationProvider.getApplicationContext<Context>()
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
prefs.edit().putBoolean(context.getString(R.string.settings_key_wear_sync), true).commit()
WearSyncServiceManager.onPermissionResult(context, true)
assertTrue(prefs.getBoolean(context.getString(R.string.settings_key_wear_sync), false))
}
}
-2
View File
@@ -2,9 +2,7 @@
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 {
+8 -36
View File
@@ -5,16 +5,6 @@ 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
@@ -32,17 +22,13 @@ if [ -z "${JAVA_HOME:-}" ]; then
fi
echo "Starting build"
./gradlew clean ":${BUILD_TYPE}:assembleRelease"
./gradlew clean assembleRelease
echo "Build finished (unsigned)"
flavourDirs=$(find "${BUILD_TYPE}/build/outputs/apk/" -mindepth 1 -maxdepth 1 -type d)
flavourDirs=$(find app/build/outputs/apk/ -mindepth 1 -maxdepth 1 -type d)
for flavourDir in $flavourDirs; do
flavourName="$(basename "$flavourDir")"
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
echo "Your $flavourName flavour is at $flavourDir/release/app-$flavourName-release-unsigned.apk"
done
if [ -z "${KEYSTORE:-}" ]; then
@@ -58,33 +44,19 @@ else
for flavourDir in $flavourDirs; do
flavourName="$(basename "$flavourDir")"
echo "Signing $flavourName flavour..."
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
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"
echo "Build finished (signed)"
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
echo "Your $flavourName flavour is at $flavourDir/release/app-$flavourName-release.apk"
done
shasumPath="$(pwd)/SHA256SUMS"
echo "" > "$shasumPath"
for flavourDir in $flavourDirs; do
flavourName="$(basename "$flavourDir")"
if [ "$flavourName" == "release" ]; then
pushd "$flavourDir"
else
pushd "$flavourDir/release/"
fi
sha256sum -- "${BUILD_TYPE}"-*.apk >> "$shasumPath"
pushd "$flavourDir/release/"
sha256sum -- *.apk >> "$shasumPath"
popd
done
-97
View File
@@ -1,97 +0,0 @@
# 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
}
]
}
```
+18 -22
View File
@@ -1,13 +1,9 @@
# Content Provider (for Gadgetbridge and other local apps)
# Content Provider
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:
@@ -45,23 +41,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` | `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 |
| 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 |
### /groups
+5 -22
View File
@@ -1,31 +1,14 @@
**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 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)
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
9. Push the version update commit: `git push`
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.
10. Create a new release on GitHub and attach the `app/build/outputs/apk/foss/release/app-foss-release.apk` and `SHA256SUMS` files
11. When pushing the release to Google Play Production, update the metadata there: `bundle exec fastlane supply --version_code <VERSION_CODE>`
@@ -1,3 +0,0 @@
- 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ů
@@ -1 +0,0 @@
- Přidána možnost otevření obrázku v interním prohlížeči obrázků v galerii
@@ -1,3 +0,0 @@
- 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
@@ -1 +0,0 @@
- Option hinzugefügt, um Bilder aus dem integrierten Bildbetrachter in der Galerie App zu öffnen
@@ -1,2 +0,0 @@
- Add a Bluetooth server for Wear OS companion app support
- Remove embedded CHANGELOG
@@ -1 +0,0 @@
- Add option to open image in internal image viewer in gallery
@@ -1 +0,0 @@
- Initial release of Wear OS companion app
@@ -11,7 +11,6 @@ With this essential everyday carry (EDC) tool you can replace useless plastic wi
- Import cards and codes from files, Catima, FidMe, Loyalty Card Keychain and Voucher Vault.
- Make a backup of all your cards and transfer them to a new device if you want.
- Share coupons, exclusive offers, promo codes, or cards and codes using any app.
- Sync with supported Gadgetbridge or Wear OS smartwatches.
- Dark theme and accessibility options for vision impaired users.
- Made for everyone by the libre software community.
- Localized hand-made translations for 40+ languages.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

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