Compare commits

..

1 Commits

Author SHA1 Message Date
TheLastProject
760bdc78e4 Update feature graphic 2025-08-22 15:42:23 +00:00
675 changed files with 14915 additions and 7830 deletions

View File

@@ -9,15 +9,10 @@ updates:
- mavenCentral
schedule:
interval: "daily"
cooldown:
default-days: 7
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 7
# Workaround for https://github.com/dependabot/dependabot-core/issues/6888
registries:

View File

@@ -32,8 +32,10 @@ jobs:
matrix:
flavor: [Foss, Gplay]
steps:
- uses: actions/checkout@v6
- uses: gradle/actions/wrapper-validation@v5
- uses: actions/checkout@v5.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
- uses: gradle/actions/wrapper-validation@v4
- name: set up OpenJDK 21
run: |
sudo apt-get update
@@ -64,7 +66,7 @@ jobs:
script: ./gradlew connected${{ matrix.flavor }}DebugAndroidTest
- name: Archive test results
if: always()
uses: actions/upload-artifact@v6.0.0
uses: actions/upload-artifact@v4.6.2
with:
name: test-results-flavor${{ matrix.flavor }}
path: app/build/reports

View File

@@ -1,5 +1,4 @@
name: Convert CHANGELOG to Fastlane
on:
workflow_dispatch:
push:
@@ -7,11 +6,20 @@ on:
- main
paths:
- 'CHANGELOG.md'
permissions:
actions: none
checks: none
contents: write
deployments: none
discussions: none
id-token: none
issues: none
packages: none
pages: none
pull-requests: write
repository-projects: none
security-events: none
statuses: none
jobs:
convert_changelog_to_fastlane:
runs-on: ubuntu-latest
@@ -19,15 +27,15 @@ jobs:
steps:
- name: Checkout repo
id: checkout
uses: actions/checkout@v6
uses: actions/checkout@v5.0.0
- name: Setup Python
uses: actions/setup-python@v6.1.0
uses: actions/setup-python@v5.6.0
with:
python-version: '3.x'
- name: Run converter script
run: python .scripts/changelog_to_fastlane.py
- name: Create Pull Request
uses: peter-evans/create-pull-request@v8.0.0
uses: peter-evans/create-pull-request@v7.0.8
with:
title: "Update Fastlane changelogs"
commit-message: "Update Fastlane changelogs"

View File

@@ -1,14 +1,22 @@
name: Write contributors to file
on:
workflow_dispatch:
schedule:
- cron: '3 4 * * 0'
permissions:
actions: none
checks: none
contents: write
deployments: none
discussions: none
id-token: none
issues: none
packages: none
pages: none
pull-requests: write
repository-projects: none
security-events: none
statuses: none
jobs:
contributors_to_file:
runs-on: ubuntu-latest
@@ -17,7 +25,7 @@ jobs:
steps:
- name: Checkout repo
id: checkout
uses: actions/checkout@v6
uses: actions/checkout@v5.0.0
- name: Update contributors
id: update_contributors
uses: TheLastProject/contributors-to-file-action@v3.2.0
@@ -25,7 +33,7 @@ jobs:
file_in_repo: app/src/main/res/raw/contributors.txt
min_commit_count: 5
- name: Create Pull Request
uses: peter-evans/create-pull-request@v8.0.0
uses: peter-evans/create-pull-request@v7.0.8
with:
title: "Update contributors"
commit-message: "Update contributors"

View File

@@ -1,5 +1,4 @@
name: Generate feature graphic
on:
workflow_dispatch:
push:
@@ -8,16 +7,25 @@ on:
paths:
- 'fastlane/**/title.txt'
- '.scripts/generate_feature_graphic/**'
permissions:
actions: none
checks: none
contents: write
deployments: none
discussions: none
id-token: none
issues: none
packages: none
pages: none
pull-requests: write
repository-projects: none
security-events: none
statuses: none
jobs:
generate-feature-graphic:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5.0.0
- name: Install requirements
run: |
sudo apt-get update
@@ -31,7 +39,7 @@ jobs:
- name: Generate featureGraphic.png for each language
run: .scripts/generate_feature_graphic/generate_feature_graphic.sh
- name: Create Pull Request
uses: peter-evans/create-pull-request@v8.0.0
uses: peter-evans/create-pull-request@v7.0.8
with:
title: "Update feature graphic"
commit-message: "Update feature graphic"

View File

@@ -1,34 +0,0 @@
name: i18n check
on:
workflow_dispatch:
push:
branches:
- main
- staging
- trying
pull_request:
branches:
- main
permissions:
actions: none
checks: none
contents: read
deployments: none
discussions: none
id-token: none
issues: none
packages: none
pages: none
pull-requests: none
repository-projects: none
security-events: none
statuses: none
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- 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
run: bash .scripts/check_app_name.sh

View File

@@ -1,19 +0,0 @@
name: Update Gradle Wrapper
on:
schedule:
- cron: "0 0 * * *"
permissions:
contents: write
pull-requests: write
jobs:
update-gradle-wrapper:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Update Gradle Wrapper
uses: gradle-update/update-gradle-wrapper-action@v2

View File

@@ -1,5 +1,4 @@
name: Update locales
on:
workflow_dispatch:
push:
@@ -8,22 +7,31 @@ on:
paths:
- app/src/main/res/values-*/strings.xml
- app/src/main/res/values/settings.xml
permissions:
actions: none
checks: none
contents: write
deployments: none
discussions: none
id-token: none
issues: none
packages: none
pages: none
pull-requests: write
repository-projects: none
security-events: none
statuses: none
jobs:
update-locales:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5.0.0
- name: Add new locales
run: .scripts/new-locales.py
- name: Update locales
run: .scripts/locales.py
- name: Create Pull Request
uses: peter-evans/create-pull-request@v8.0.0
uses: peter-evans/create-pull-request@v7.0.8
with:
title: "Update locales"
commit-message: "Update locales"

4
.gitignore vendored
View File

@@ -19,8 +19,8 @@
/app/*.log
/app/build
/app/release
/.idea/*
!/.idea/icon.svg
/.idea
# Bundle
/.bundle/
/vendor/bundle

1
.idea/icon.svg generated
View File

@@ -1 +0,0 @@
../.design/ic_launcher_foreground.svg

View File

@@ -1,71 +0,0 @@
#!/bin/bash
set -e
shopt -s lastpipe # Run last command in a pipeline in the current shell.
# Colors
LIGHTCYAN='\033[1;36m'
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Vars
SUCCESS=1
CANONICAL_TITLE="Catima"
ALLOWLIST=("ar" "bn" "fa" "fa-IR" "he-IL" "hi" "hi-IN" "kn" "kn-IN" "ml" "mr" "ta" "ta-IN" "zh-rTW" "zh-TW") # TODO: Link values and fastlane with different codes together
function get_lang() {
LANG_DIRNAME=$(dirname "$FILE" | xargs basename)
LANG=${LANG_DIRNAME#values-} # Fetch lang name
LANG=${LANG#values} # Handle "app/src/main/res/values"
LANG=${LANG:-en} # Default to en
}
# FIXME: This function should use its own variables and return a success/fail status, instead of working on global variables
function check() {
# FIXME: This allows inconsistency between values and fastlane if the app name is not Catima
# When the app name is not Catima, it should still check if title.txt and strings.xml use the same app name (or start)
if echo "${ALLOWLIST[*]}" | grep -w -q "${LANG}" || [[ -z ${APP_NAME} ]]; then
return 0
fi
if [[ ${FILE} == *"title.txt" ]]; then
if [[ ! ${APP_NAME} =~ ^${CANONICAL_TITLE} ]]; then
echo -e "${RED}Error: ${LIGHTCYAN}title in $FILE ($LANG) is ${RED}'$APP_NAME'${LIGHTCYAN}, expected to start with ${GREEN}'$CANONICAL_TITLE'. ${NC}"
SUCCESS=0
fi
else
if [[ ${APP_NAME} != "${CANONICAL_TITLE}" ]]; then
echo -e "${RED}Error: ${LIGHTCYAN}app_name in $FILE ($LANG) is ${RED}'$APP_NAME'${LIGHTCYAN}, expected ${GREEN}'$CANONICAL_TITLE'. ${NC}"
SUCCESS=0
fi
fi
}
# FIXME: This checks all title.txt and strings.xml files separately, but it needs to check if the title.txt and strings.xml match for a language as well
echo -e "${LIGHTCYAN}Checking title.txt's. ${NC}"
find fastlane/metadata/android/* -maxdepth 1 -type f -name "title.txt" | while read -r FILE; do
APP_NAME=$(head -n 1 "$FILE")
get_lang
check
done
echo -e "${LIGHTCYAN}Checking string.xml's. ${NC}"
find app/src/main/res/values* -maxdepth 1 -type f -name "strings.xml" | while read -r FILE; do
# FIXME: This only checks app_name, but there are more strings with Catima inside it
# It should check the original English text for all strings that contain Catima and ensure they use the correct app_name for consistency
APP_NAME=$(grep -oP '<string name="app_name">\K[^<]+' "$FILE" | head -n1)
get_lang
check
done
if [[ $SUCCESS -eq 1 ]]; then
echo -e "\n${GREEN}Success!! All app_name values match the canonical title. ${NC}"
else
echo -e "\n${RED}Unsuccessful!! Some app_name values did not match the canonical titles. ${NC}"
exit 1
fi

44
.scripts/dump_stocard_stores.py Executable file
View File

@@ -0,0 +1,44 @@
#!/usr/bin/python3
import csv
import json
import msgpack
MSGPACK = "bootstrapdata.msgpack"
OUTFILE = "stocard_stores.csv"
def load(fh):
data = []
for r in msgpack.Unpacker(fh, raw=False):
if r["collection"] == "/loyalty-card-providers/":
d = json.loads(r["data"])
data.append([r["resource_id"], d["name"], d["default_barcode_format"]])
return data
def save(data, output_file=OUTFILE):
with open(output_file, "w") as fh:
writer = csv.writer(fh, lineterminator="\n")
writer.writerow(["_id", "name", "barcodeFormat"])
for row in data:
writer.writerow(row)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
epilog=f"INPUT_FILE must be a .msgpack or .apk and defaults to {MSGPACK}; "
f"OUTPUT_FILE defaults to {OUTFILE}")
parser.add_argument("input_file", metavar="INPUT_FILE", nargs="?", default=MSGPACK)
parser.add_argument("output_file", metavar="OUTPUT_FILE", nargs="?", default=OUTFILE)
args = parser.parse_args()
if args.input_file.lower().endswith(".apk"):
import zipfile
with zipfile.ZipFile(args.input_file) as zf:
with zf.open(f"assets/{MSGPACK}") as fh:
data = load(fh)
else:
with open(args.input_file, "rb") as fh:
data = load(fh)
save(data, args.output_file)

View File

@@ -42,7 +42,6 @@ for lang in "$script_location/../../fastlane/metadata/android/"*; do
ja-JP) sed -i "s/Lexend/Noto Sans CJK JP/" featureGraphic.svg ;;
kn-IN) sed -i -e 's/font-size="150"/font-size="125"/' -e 's/\(<tspan x="469" \)y="270"/\1y="240"/' -e "s/Lobster/Noto Sans Kannada/" -e "s/Lexend/Noto Sans Kannada/" featureGraphic.svg ;;
ko) sed -i "s/Lexend/Noto Sans CJK KR/" featureGraphic.svg ;;
ta-IN) sed -i -e 's/font-size="150"/font-size="125"/' -e 's/\(<tspan x="469" \)y="270"/\1y="240"/' featureGraphic.svg ;;
zh-CN) sed -i "s/Lexend/Noto Sans CJK SC/" featureGraphic.svg ;;
zh-TW) sed -i -e "s/Lobster/Noto Sans CJK TC/" -e "s/Lexend/Noto Sans CJK TC/" featureGraphic.svg ;;
*) ;;

View File

@@ -1,39 +1,6 @@
# Changelog
## Unreleased - 157
- Add support for UTF-8 barcodes
- Add duplicate option to main screen and reorder options slightly
- Fix column count setting not being applied to group card list
- Reduce max photo size to reduce storage use (only for newly added photos)
## v2.40.0 - 156 (2025-12-08)
- Copy card ID to clipboard from view dialog or long press
- Swap balance and currency fields to hopefully reduce unintended rounding
## v2.39.2 - 155 (2025-11-04)
- Preparations for future improvements (rewrote many classes to Kotlin)
## v2.39.1 - 154 (2025-10-01)
- Fix possible crash that could occur for cards missing colour information in the database
## v2.39.0 - 153 (2025-09-30)
- Target Android 16
- Fix possible crash after removing image from card
- Remove "Screen orientation" feature (Google removed the ability for apps to control screen rotation when targeting Android 16)
- Add crash reporter to FOSS build (not used in Google Play version, only in other app stores)
## v2.38.0 - 152 (2025-09-12)
- Add support for .pkpasses files
- Remove Stocard importer (Stocard no longer exists)
- Temporarily disable widget images below Android 12L (workaround for a crash issue)
## v2.37.0 - 151 (2025-08-22)
## Unreleased - 151
- New redesign of the Catima logo
- Translation updates

View File

@@ -23,30 +23,6 @@ for good reason.
## Code Changes
Note: submitting LLM ("AI") generated code is strongly discouraged, as such
code is often (subtly) incorrect or overcomplicated (for example: unnecessarily
pulling in extra libraries for functionality already covered by existing
libraries). It also often makes unrelated changes that increase the risk of
introducing new issues and complicates reviewing. Even when it doesn't do any
of the before mentioned things, it will often not fit the coding style and flow
of existing code, requiring excessive refactoring.
While we cannot ever control or be sure if LLMs were used to generate the
submitted code, it is your responsibility to ensure that whatever code you
submit is correct and fits within the design of existing code. It is never
acceptable to defend a change by stating a LLM suggested it.
This is a personal plea more than anything: please understand that writing code
is the easy part. The hard part is making sure the code fits the design of the
rest of the application and is maintainable. Reviewing is a very time-consuming
task for this reason. Please do not use LLMs to quickly generate a "fix" and
moving the cost of labor to me as a reviewer. If you do use LLMs to generate
part of your code, please be open about this, explain what was generated how
and how you confirmed and refactored the code to fit the project and minimized
risk.
Please never submit LLM-generated code as-is.
### Test Your Code
There are four possible tests you can run to verify your code. The first

View File

@@ -1,5 +1,5 @@
**Last updated**
September 30 2025
August 30 2023
# Privacy Policy
Catima does not collect or transmit any personal information.
@@ -11,12 +11,6 @@ To ensure correct app functionality, we require access to the following:
Catima offers a feature to share cards with other users. All the relevant data is in the generated shareable URLs and never transmitted to our servers. When viewed through catima.app, the data in the URL is rendered using client-side Javascript to further ensure no data is ever transmitted to us.
## Crash reporting privacy
In the FOSS version of Catima (the version used on IzzyOnDroid, F-Droid and GitHub), the open source crash reporter ACRA is used for crash reporting. When a crash is detected, Catima will ask the user if they are willing to report the crash. If they choose to do so, the user's mail client is opened so they can review the data that would be sent. Crash reporting data is only sent when the user explicitly chooses to do so, it is **never** sent automatically. Crash reporting data is only used to solve crashes and no (potentially) sensitive information is ever shared. Users who do not want to be asked to report crashes can disable the "Ask to send crash reports" setting in Catima settings.
For the Google Play version of Catima, crash reporting is [managed by Google](https://support.google.com/googleplay/android-developer/answer/9859174?hl=en). Users can opt in or out of crash reporting through the Google app under the "Usage and diagnostics" setting.
# Changes
This Privacy Policy may be updated from time to time for any reason. We will notify you of any changes to our Privacy Policy by posting the new Privacy Policy to https://catima.app/privacy-policy/. A snapshot of the Privacy Policy is available within the Catima app, though it may be outdated. When the Privacy Policy on the website and in the app differ, the website should be considered leading. You are advised to consult the Privacy Policy regularly for any changes, as continued use is deemed approval of all changes.

View File

@@ -1,10 +1,8 @@
import com.android.build.gradle.internal.tasks.factory.dependsOn
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.com.android.application)
alias(libs.plugins.org.jetbrains.kotlin.android)
alias(libs.plugins.org.jetbrains.kotlin.plugin.compose)
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
kotlin {
@@ -13,14 +11,14 @@ kotlin {
android {
namespace = "protect.card_locker"
compileSdk = 36
compileSdk = 35
defaultConfig {
applicationId = "me.hackerchick.catima"
minSdk = 21
targetSdk = 36
versionCode = 156
versionName = "2.40.0"
targetSdk = 35
versionCode = 150
versionName = "2.36.0"
vectorDrawables.useSupportLibrary = true
multiDexEnabled = true
@@ -31,7 +29,6 @@ android {
buildConfigField("boolean", "showDonate", "true")
buildConfigField("boolean", "showRateOnGooglePlay", "false")
buildConfigField("boolean", "useAcraCrashReporter", "true")
}
buildTypes {
@@ -49,7 +46,6 @@ android {
buildFeatures {
buildConfig = true
compose = true
viewBinding = true
}
@@ -65,9 +61,6 @@ android {
// Google doesn't allow donation links
buildConfigField("boolean", "showDonate", "false")
buildConfigField("boolean", "showRateOnGooglePlay", "true")
// Google Play already sends crashes to the Google Play Console
buildConfigField("boolean", "useAcraCrashReporter", "false")
}
}
@@ -77,6 +70,16 @@ android {
}
}
compileOptions {
encoding = "UTF-8"
// Flag to enable support for the new language APIs
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
sourceSets {
getByName("test") {
resources.srcDirs("src/test/res")
@@ -95,73 +98,49 @@ android {
lint {
lintConfig = file("lint.xml")
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_17
}
kotlinOptions {
jvmTarget = "21"
}
compileOptions {
encoding = "UTF-8"
// Flag to enable support for the new language APIs
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
}
dependencies {
// AndroidX
implementation(libs.androidx.appcompat.appcompat)
implementation(libs.androidx.constraintlayout.constraintlayout)
implementation(libs.androidx.core.core.ktx)
implementation(libs.androidx.core.core.remoteviews)
implementation(libs.androidx.core.core.splashscreen)
implementation(libs.androidx.exifinterface.exifinterface)
implementation(libs.androidx.palette.palette)
implementation(libs.androidx.preference.preference)
implementation(libs.com.google.android.material.material)
coreLibraryDesugaring(libs.com.android.tools.desugar.jdk.libs)
// Compose
implementation(libs.androidx.activity.activity.compose)
val composeBom = platform(libs.androidx.compose.compose.bom)
implementation(composeBom)
implementation(libs.androidx.compose.foundation.foundation)
implementation(libs.androidx.compose.material3.material3)
implementation(libs.androidx.compose.material.material.icons.extended)
implementation(libs.androidx.compose.ui.ui.tooling.preview.android)
debugImplementation(libs.androidx.compose.ui.ui.test.manifest)
androidTestImplementation(composeBom)
androidTestImplementation(libs.androidx.compose.ui.ui.test.junit4)
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
implementation("androidx.core:core-ktx:1.16.0")
implementation("androidx.core:core-remoteviews:1.1.0")
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("androidx.exifinterface:exifinterface:1.4.1")
implementation("androidx.palette:palette:1.0.0")
implementation("androidx.preference:preference:1.2.1")
implementation("com.google.android.material:material:1.12.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
// Third-party
implementation(libs.com.journeyapps.zxing.android.embedded)
implementation(libs.com.github.yalantis.ucrop)
implementation(libs.com.google.zxing.core)
implementation(libs.org.apache.commons.commons.csv)
implementation(libs.com.jaredrummler.colorpicker)
implementation(libs.net.lingala.zip4j.zip4j)
// Crash reporting
implementation(libs.bundles.acra)
implementation("com.journeyapps:zxing-android-embedded:4.3.0@aar")
implementation("com.github.yalantis:ucrop:2.2.10")
implementation("com.google.zxing:core:3.5.3")
implementation("org.apache.commons:commons-csv:1.9.0")
implementation("com.jaredrummler:colorpicker:1.1.0")
implementation("net.lingala.zip4j:zip4j:2.11.5")
// Testing
testImplementation(libs.androidx.test.core)
testImplementation(libs.junit.junit)
testImplementation(libs.org.robolectric.robolectric)
val androidXTestVersion = "1.7.0"
val junitVersion = "4.13.2"
testImplementation("androidx.test:core:$androidXTestVersion")
testImplementation("junit:junit:$junitVersion")
testImplementation("org.robolectric:robolectric:4.15.1")
androidTestImplementation(libs.bundles.androidx.test)
androidTestImplementation(libs.junit.junit)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.androidx.test.rules)
androidTestImplementation(libs.androidx.test.runner)
androidTestImplementation(libs.androidx.test.uiautomator.uiautomator)
androidTestImplementation(libs.androidx.test.espresso.espresso.core)
androidTestImplementation("androidx.test:core:$androidXTestVersion")
androidTestImplementation("junit:junit:$junitVersion")
androidTestImplementation("androidx.test.ext:junit:1.3.0")
androidTestImplementation("androidx.test:runner:$androidXTestVersion")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
}
tasks.register("copyRawResFiles", Copy::class) {

View File

@@ -21,19 +21,4 @@
-keepattributes SourceFile,LineNumberTable
# This keep the class and method names the same, for debugging stack traces
-dontobfuscate
# Required for uCrop 2.2.11
# This is generated automatically by the Android Gradle plugin.
-dontwarn javax.annotation.processing.AbstractProcessor
-dontwarn javax.annotation.processing.SupportedOptions
-dontwarn okhttp3.Call
-dontwarn okhttp3.Dispatcher
-dontwarn okhttp3.OkHttpClient
-dontwarn okhttp3.Request$Builder
-dontwarn okhttp3.Request
-dontwarn okhttp3.Response
-dontwarn okhttp3.ResponseBody
-dontwarn okio.BufferedSource
-dontwarn okio.Okio
-dontwarn okio.Sink
-dontobfuscate

View File

@@ -1,89 +0,0 @@
package protect.card_locker
import android.app.Instrumentation
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.runComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import protect.card_locker.compose.theme.CatimaTheme
@OptIn(ExperimentalTestApi::class)
@RunWith(AndroidJUnit4::class)
class AboutActivityTest {
@get:Rule
private val rule: ComposeContentTestRule = createComposeRule()
private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
private val content: AboutContent = AboutContent(instrumentation.targetContext)
@Test
fun testInitialState(): Unit = runComposeUiTest {
setContent {
AboutScreenContent(content = content)
}
onNodeWithTag("topbar_catima").assertIsDisplayed()
onNodeWithTag("card_version_history").assertIsDisplayed()
onNodeWithText(content.versionHistory).assertIsDisplayed()
onNodeWithTag("card_credits").assertIsDisplayed()
onNodeWithText(content.copyrightShort).assertIsDisplayed()
onNodeWithTag("card_translate").assertIsDisplayed()
onNodeWithTag("card_license").assertIsDisplayed()
// We might be off the screen so start scrolling
onNodeWithTag("card_source_github").performScrollTo().assertIsDisplayed()
onNodeWithTag("card_privacy_policy").performScrollTo().assertIsDisplayed()
onNodeWithTag("card_donate").performScrollTo().assertIsDisplayed()
// Dont scroll to this, since its not displayed
onNodeWithTag("card_rate_google").assertIsNotDisplayed()
onNodeWithTag("card_report_error").performScrollTo().assertIsDisplayed()
}
@Test
fun testDonateAndGoogleCardVisible(): Unit = runComposeUiTest {
setContent {
CatimaTheme {
AboutScreenContent(
content = content,
showDonate = true,
showRateOnGooglePlay = true,
)
}
}
onNodeWithTag("card_donate").performScrollTo().assertIsDisplayed()
onNodeWithTag("card_rate_google").performScrollTo().assertIsDisplayed()
}
@Test
fun testDonateAndGoogleCardHidden(): Unit = runComposeUiTest {
setContent {
CatimaTheme {
AboutScreenContent(
content = content,
showDonate = false,
showRateOnGooglePlay = false,
)
}
}
onNodeWithTag("card_privacy_policy").performScrollTo().assertIsDisplayed()
onNodeWithTag("card_donate").assertIsNotDisplayed()
onNodeWithTag("card_rate_google").assertIsNotDisplayed()
onNodeWithTag("card_report_error").performScrollTo().assertIsDisplayed()
}
}

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">卡提碼除錯版</string>
</resources>
<string name="app_name">Catima 除錯版</string>
</resources>

View File

@@ -4,7 +4,7 @@
<permission
android:description="@string/permissionReadCardsDescription"
android:icon="@drawable/ic_launcher_monochrome"
android:icon="@drawable/ic_launcher_foreground"
android:label="@string/permissionReadCardsLabel"
android:name="${applicationId}.READ_CARDS"
android:protectionLevel="dangerous" />
@@ -65,7 +65,6 @@
<data android:mimeType="application/vnd.apple.pkpass" />
<data android:mimeType="application/vnd-com.apple.pkpass" />
<data android:mimeType="application/vnd.espass-espass" />
<data android:mimeType="application/vnd.apple.pkpasses" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
@@ -77,7 +76,6 @@
<data android:mimeType="application/vnd.apple.pkpass" />
<data android:mimeType="application/vnd-com.apple.pkpass" />
<data android:mimeType="application/vnd.espass-espass" />
<data android:mimeType="application/vnd.apple.pkpasses" />
</intent-filter>
</activity>
<activity
@@ -144,11 +142,12 @@
android:name=".preferences.SettingsActivity"
android:label="@string/settings"
android:theme="@style/AppTheme.NoActionBar" />
<!-- FIXME: ImportExportActivity cancels import on rotation -->
<!-- FIXME: locked screenOrientation is a workaround for https://github.com/CatimaLoyalty/Android/issues/1715, remove when https://github.com/CatimaLoyalty/Android/issues/513 is fixed -->
<activity
android:name=".ImportExportActivity"
android:label="@string/importExport"
android:exported="true"
android:screenOrientation="locked"
android:theme="@style/AppTheme.NoActionBar">
<!-- ZIP Intent Filter -->

View File

@@ -1,167 +1,149 @@
package protect.card_locker
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.fromHtml
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import protect.card_locker.compose.CatimaAboutSection
import protect.card_locker.compose.CatimaTopAppBar
import protect.card_locker.compose.theme.CatimaTheme
import android.text.Spanned
import android.view.MenuItem
import android.view.View
import android.widget.ScrollView
import android.widget.TextView
class AboutActivity : ComponentActivity() {
import androidx.annotation.StringRes
import androidx.core.view.isVisible
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import protect.card_locker.databinding.AboutActivityBinding
class AboutActivity : CatimaAppCompatActivity() {
private companion object {
private const val TAG = "Catima"
}
private lateinit var binding: AboutActivityBinding
private lateinit var content: AboutContent
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = AboutActivityBinding.inflate(layoutInflater)
content = AboutContent(this)
title = content.pageTitle
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
enableToolbarBackButton()
setContent {
CatimaTheme {
AboutScreenContent(
content = content,
showDonate = BuildConfig.showDonate,
showRateOnGooglePlay = BuildConfig.showRateOnGooglePlay,
onBackPressedDispatcher = onBackPressedDispatcher
)
binding.apply {
creditsSub.text = content.copyrightShort
versionHistorySub.text = content.versionHistory
versionHistory.tag = "https://catima.app/changelog/"
translate.tag = "https://hosted.weblate.org/engage/catima/"
license.tag = "https://github.com/CatimaLoyalty/Android/blob/main/LICENSE"
repo.tag = "https://github.com/CatimaLoyalty/Android/"
privacy.tag = "https://catima.app/privacy-policy/"
reportError.tag = "https://github.com/CatimaLoyalty/Android/issues"
rate.tag = "https://play.google.com/store/apps/details?id=me.hackerchick.catima"
donate.tag = "https://catima.app/donate"
// Hide Google Play rate button if not on Google Play
rate.isVisible = BuildConfig.showRateOnGooglePlay
// Hide donate button on Google Play (Google Play doesn't allow donation links)
donate.isVisible = BuildConfig.showDonate
}
bindClickListeners()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
finish()
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onDestroy() {
super.onDestroy()
content.destroy()
clearClickListeners()
}
private fun bindClickListeners() {
binding.apply {
versionHistory.setOnClickListener { showHistory(it) }
translate.setOnClickListener { openExternalBrowser(it) }
license.setOnClickListener { showLicense(it) }
repo.setOnClickListener { openExternalBrowser(it) }
privacy.setOnClickListener { showPrivacy(it) }
reportError.setOnClickListener { openExternalBrowser(it) }
rate.setOnClickListener { openExternalBrowser(it) }
donate.setOnClickListener { openExternalBrowser(it) }
credits.setOnClickListener { showCredits() }
}
}
private fun clearClickListeners() {
binding.apply {
versionHistory.setOnClickListener(null)
translate.setOnClickListener(null)
license.setOnClickListener(null)
repo.setOnClickListener(null)
privacy.setOnClickListener(null)
reportError.setOnClickListener(null)
rate.setOnClickListener(null)
donate.setOnClickListener(null)
credits.setOnClickListener(null)
}
}
private fun showCredits() {
showHTML(R.string.credits, content.contributorInfo, null)
}
private fun showHistory(view: View) {
showHTML(R.string.version_history, content.historyInfo, view)
}
private fun showLicense(view: View) {
showHTML(R.string.license, content.licenseInfo, view)
}
private fun showPrivacy(view: View) {
showHTML(R.string.privacy_policy, content.privacyInfo, view)
}
private fun showHTML(@StringRes title: Int, text: Spanned, view: View?) {
val dialogContentPadding = resources.getDimensionPixelSize(R.dimen.alert_dialog_content_padding)
val textView = TextView(this).apply {
setText(text)
Utils.makeTextViewLinksClickable(this, text)
}
val scrollView = ScrollView(this).apply {
addView(textView)
setPadding(dialogContentPadding, dialogContentPadding / 2, dialogContentPadding, 0)
}
MaterialAlertDialogBuilder(this).apply {
setTitle(title)
setView(scrollView)
setPositiveButton(R.string.ok, null)
// Add View online button if an URL is linked to this view
view?.tag?.let {
setNeutralButton(R.string.view_online) { _, _ -> openExternalBrowser(view) }
}
show()
}
}
private fun openExternalBrowser(view: View) {
val tag = view.tag
if (tag is String && tag.startsWith("https://")) {
OpenWebLinkHandler().openBrowser(this, tag)
}
}
}
@Composable
fun AboutScreenContent(
content: AboutContent,
showDonate: Boolean = true,
showRateOnGooglePlay: Boolean = false,
onBackPressedDispatcher: OnBackPressedDispatcher? = null,
) {
Scaffold(
topBar = { CatimaTopAppBar(content.pageTitle.toString(), onBackPressedDispatcher) }
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.verticalScroll(rememberScrollState())
) {
CatimaAboutSection(
stringResource(R.string.version_history),
content.versionHistory,
modifier = Modifier.testTag("card_version_history"),
onClickUrl = "https://catima.app/changelog/",
onClickDialogText = AnnotatedString.fromHtml(
htmlString = content.historyHtml,
linkStyles = TextLinkStyles(
style = SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colorScheme.primary
)
)
)
)
CatimaAboutSection(
stringResource(R.string.credits),
content.copyrightShort,
modifier = Modifier.testTag("card_credits"),
onClickDialogText = AnnotatedString.fromHtml(
htmlString = content.contributorInfoHtml,
linkStyles = TextLinkStyles(
style = SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colorScheme.primary
)
)
)
)
CatimaAboutSection(
stringResource(R.string.help_translate_this_app),
stringResource(R.string.translate_platform),
modifier = Modifier.testTag("card_translate"),
onClickUrl = "https://hosted.weblate.org/engage/catima/"
)
CatimaAboutSection(
stringResource(R.string.license),
stringResource(R.string.app_license),
modifier = Modifier.testTag("card_license"),
onClickUrl = "https://github.com/CatimaLoyalty/Android/blob/main/LICENSE",
onClickDialogText = AnnotatedString.fromHtml(
htmlString = content.licenseHtml,
linkStyles = TextLinkStyles(
style = SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colorScheme.primary
)
)
)
)
CatimaAboutSection(
stringResource(R.string.source_repository),
stringResource(R.string.on_github),
modifier = Modifier.testTag("card_source_github"),
onClickUrl = "https://github.com/CatimaLoyalty/Android/"
)
CatimaAboutSection(
stringResource(R.string.privacy_policy),
stringResource(R.string.and_data_usage),
modifier = Modifier.testTag("card_privacy_policy"),
onClickUrl = "https://catima.app/privacy-policy/",
onClickDialogText = AnnotatedString.fromHtml(
htmlString = content.privacyHtml,
linkStyles = TextLinkStyles(
style = SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colorScheme.primary
)
)
)
)
if (showDonate) {
CatimaAboutSection(
stringResource(R.string.donate),
"",
modifier = Modifier.testTag("card_donate"),
onClickUrl = "https://catima.app/donate"
)
}
if (showRateOnGooglePlay) {
CatimaAboutSection(
stringResource(R.string.rate_this_app),
stringResource(R.string.on_google_play),
modifier = Modifier.testTag("card_rate_google"),
onClickUrl = "https://play.google.com/store/apps/details?id=me.hackerchick.catima"
)
}
CatimaAboutSection(
stringResource(R.string.report_error),
stringResource(R.string.on_github),
modifier = Modifier.testTag("card_report_error"),
onClickUrl = "https://github.com/CatimaLoyalty/Android/issues"
)
}
}
}
@Preview
@Composable
private fun AboutActivityPreview() {
AboutScreenContent(AboutContent(LocalContext.current))
}

View File

@@ -3,8 +3,11 @@ package protect.card_locker;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.text.Spanned;
import android.util.Log;
import androidx.core.text.HtmlCompat;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
@@ -52,7 +55,7 @@ public class AboutContent {
return context.getString(R.string.app_copyright_short);
}
public String getContributorsHtml() {
public String getContributors() {
String contributors;
try {
contributors = "<br/>" + Utils.readTextFile(context, R.raw.contributors);
@@ -62,7 +65,7 @@ public class AboutContent {
return contributors.replace("\n", "<br />");
}
public String getHistoryHtml() {
public String getHistory() {
String versionHistory;
try {
versionHistory = Utils.readTextFile(context, R.raw.changelog)
@@ -74,7 +77,7 @@ public class AboutContent {
.replace("\n", "<br />");
}
public String getLicenseHtml() {
public String getLicense() {
try {
return Utils.readTextFile(context, R.raw.license);
} catch (IOException ignored) {
@@ -82,7 +85,7 @@ public class AboutContent {
}
}
public String getPrivacyHtml() {
public String getPrivacy() {
String privacyPolicy;
try {
privacyPolicy = Utils.readTextFile(context, R.raw.privacy)
@@ -94,11 +97,11 @@ public class AboutContent {
.replace("\n", "<br />");
}
public String getThirdPartyLibrariesHtml() {
public String getThirdPartyLibraries() {
final List<ThirdPartyInfo> usedLibraries = new ArrayList<>();
usedLibraries.add(new ThirdPartyInfo("ACRA", "https://github.com/ACRA/acra", "Apache 2.0"));
usedLibraries.add(new ThirdPartyInfo("Color Picker", "https://github.com/jaredrummler/ColorPicker", "Apache 2.0"));
usedLibraries.add(new ThirdPartyInfo("Commons CSV", "https://commons.apache.org/proper/commons-csv/", "Apache 2.0"));
usedLibraries.add(new ThirdPartyInfo("NumberPickerPreference", "https://github.com/invissvenska/NumberPickerPreference", "GNU LGPL 3.0"));
usedLibraries.add(new ThirdPartyInfo("uCrop", "https://github.com/Yalantis/uCrop", "Apache 2.0"));
usedLibraries.add(new ThirdPartyInfo("Zip4j", "https://github.com/srikanth-lingala/zip4j", "Apache 2.0"));
usedLibraries.add(new ThirdPartyInfo("ZXing", "https://github.com/zxing/zxing", "Apache 2.0"));
@@ -113,7 +116,7 @@ public class AboutContent {
return result.toString();
}
public String getUsedThirdPartyAssetsHtml() {
public String getUsedThirdPartyAssets() {
final List<ThirdPartyInfo> usedAssets = new ArrayList<>();
usedAssets.add(new ThirdPartyInfo("Android icons", "https://fonts.google.com/icons?selected=Material+Icons", "Apache 2.0"));
@@ -126,19 +129,31 @@ public class AboutContent {
return result.toString();
}
public String getContributorInfoHtml() {
public Spanned getContributorInfo() {
StringBuilder contributorInfo = new StringBuilder();
contributorInfo.append(getCopyright());
contributorInfo.append("<br/><br/>");
contributorInfo.append(context.getString(R.string.app_copyright_old));
contributorInfo.append("<br/><br/>");
contributorInfo.append(String.format(context.getString(R.string.app_contributors), getContributorsHtml()));
contributorInfo.append(String.format(context.getString(R.string.app_contributors), getContributors()));
contributorInfo.append("<br/><br/>");
contributorInfo.append(String.format(context.getString(R.string.app_libraries), getThirdPartyLibrariesHtml()));
contributorInfo.append(String.format(context.getString(R.string.app_libraries), getThirdPartyLibraries()));
contributorInfo.append("<br/><br/>");
contributorInfo.append(String.format(context.getString(R.string.app_resources), getUsedThirdPartyAssetsHtml()));
contributorInfo.append(String.format(context.getString(R.string.app_resources), getUsedThirdPartyAssets()));
return contributorInfo.toString();
return HtmlCompat.fromHtml(contributorInfo.toString(), HtmlCompat.FROM_HTML_MODE_COMPACT);
}
public Spanned getHistoryInfo() {
return HtmlCompat.fromHtml(getHistory(), HtmlCompat.FROM_HTML_MODE_COMPACT);
}
public Spanned getLicenseInfo() {
return HtmlCompat.fromHtml(getLicense(), HtmlCompat.FROM_HTML_MODE_LEGACY);
}
public Spanned getPrivacyInfo() {
return HtmlCompat.fromHtml(getPrivacy(), HtmlCompat.FROM_HTML_MODE_COMPACT);
}
public String getVersionHistory() {

View File

@@ -0,0 +1,5 @@
package protect.card_locker;
public interface BarcodeImageWriterResultCallback {
void onBarcodeImageWriterResult(boolean success);
}

View File

@@ -1,5 +0,0 @@
package protect.card_locker
interface BarcodeImageWriterResultCallback {
fun onBarcodeImageWriterResult(success: Boolean)
}

View File

@@ -4,24 +4,17 @@ import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.util.ArrayMap;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.StringUtils;
import java.lang.ref.WeakReference;
import java.nio.charset.Charset;
import java.util.Map;
import protect.card_locker.async.CompatCallable;
@@ -46,7 +39,6 @@ public class BarcodeImageWriterTask implements CompatCallable<Bitmap> {
private final WeakReference<TextView> textViewReference;
private String cardId;
private final CatimaBarcode format;
private final Charset encoding;
private final int imageHeight;
private final int imageWidth;
private final int imagePadding;
@@ -56,7 +48,7 @@ public class BarcodeImageWriterTask implements CompatCallable<Bitmap> {
BarcodeImageWriterTask(
Context context, ImageView imageView, String cardIdString,
CatimaBarcode barcodeFormat, @Nullable Charset barcodeEncoding, TextView textView,
CatimaBarcode barcodeFormat, TextView textView,
boolean showFallback, BarcodeImageWriterResultCallback callback, boolean roundCornerPadding, boolean isFullscreen
) {
mContext = context;
@@ -70,7 +62,6 @@ public class BarcodeImageWriterTask implements CompatCallable<Bitmap> {
cardId = cardIdString;
format = barcodeFormat;
encoding = barcodeEncoding;
int imageViewHeight = imageView.getHeight();
int imageViewWidth = imageView.getWidth();
@@ -181,22 +172,10 @@ public class BarcodeImageWriterTask implements CompatCallable<Bitmap> {
}
MultiFormatWriter writer = new MultiFormatWriter();
Map<EncodeHintType, Object> encodeHints = new ArrayMap<>();
// Use charset if defined or guess otherwise
if (encoding != null) {
Log.d(TAG, "Encoding explicitly set, " + encoding.name());
encodeHints.put(EncodeHintType.CHARACTER_SET, encoding);
} else {
String guessedEncoding = StringUtils.guessEncoding(cardId.getBytes(), new ArrayMap<>());
Log.d(TAG, "Guessed encoding: " + guessedEncoding);
encodeHints.put(EncodeHintType.CHARACTER_SET, Charset.forName(guessedEncoding));
}
BitMatrix bitMatrix;
try {
try {
bitMatrix = writer.encode(cardId, format.format(), imageWidth, imageHeight, encodeHints);
bitMatrix = writer.encode(cardId, format.format(), imageWidth, imageHeight, null);
} catch (Exception e) {
// Cast a wider net here and catch any exception, as there are some
// cases where an encoder may fail if the data is invalid for the

View File

@@ -92,13 +92,13 @@ public class BarcodeSelectorAdapter extends ArrayAdapter<CatimaBarcodeWithValue>
Log.d(TAG, "Generating barcode for type " + formatType);
BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getContext(), image, cardId, format, null, text, true, null, true, false);
BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getContext(), image, cardId, format, text, true, null, true, false);
mTasks.executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter);
}
});
} else {
Log.d(TAG, "Generating barcode for type " + formatType);
BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getContext(), image, cardId, format, null, text, true, null, true, false);
BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getContext(), image, cardId, format, text, true, null, true, false);
mTasks.executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter);
}
}

View File

@@ -10,11 +10,8 @@ import android.database.sqlite.SQLiteOpenHelper;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import java.io.FileNotFoundException;
import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Currency;
@@ -26,7 +23,7 @@ import java.util.Set;
public class DBHelper extends SQLiteOpenHelper {
public static final String DATABASE_NAME = "Catima.db";
public static final int ORIGINAL_DATABASE_VERSION = 1;
public static final int DATABASE_VERSION = 18;
public static final int DATABASE_VERSION = 17;
// NB: changing these values requires a migration
public static final int DEFAULT_ZOOM_LEVEL = 100;
@@ -52,7 +49,6 @@ public class DBHelper extends SQLiteOpenHelper {
public static final String CARD_ID = "cardid";
public static final String BARCODE_ID = "barcodeid";
public static final String BARCODE_TYPE = "barcodetype";
public static final String BARCODE_ENCODING = "barcodeencoding";
public static final String STAR_STATUS = "starstatus";
public static final String LAST_USED = "lastused";
public static final String ZOOM_LEVEL = "zoomlevel";
@@ -116,7 +112,6 @@ public class DBHelper extends SQLiteOpenHelper {
LoyaltyCardDbIds.CARD_ID + " TEXT not null," +
LoyaltyCardDbIds.BARCODE_ID + " TEXT," +
LoyaltyCardDbIds.BARCODE_TYPE + " TEXT," +
LoyaltyCardDbIds.BARCODE_ENCODING + " TEXT," +
LoyaltyCardDbIds.STAR_STATUS + " INTEGER DEFAULT '0'," +
LoyaltyCardDbIds.LAST_USED + " INTEGER DEFAULT '0', " +
LoyaltyCardDbIds.ZOOM_LEVEL + " INTEGER DEFAULT '" + DEFAULT_ZOOM_LEVEL + "', " +
@@ -340,11 +335,6 @@ public class DBHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE
+ " ADD COLUMN " + LoyaltyCardDbIds.ZOOM_LEVEL_WIDTH + " INTEGER DEFAULT '100' ");
}
if (oldVersion < 18 && newVersion >= 18) {
db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE
+ " ADD COLUMN " + LoyaltyCardDbIds.BARCODE_ENCODING + " TEXT");
}
}
public static Set<String> imageFiles(Context context, final SQLiteDatabase database) {
@@ -406,8 +396,7 @@ public class DBHelper extends SQLiteOpenHelper {
public static long insertLoyaltyCard(
final SQLiteDatabase database, final String store, final String note, final Date validFrom,
final Date expiry, final BigDecimal balance, final Currency balanceType, final String cardId,
final String barcodeId, final CatimaBarcode barcodeType, final @Nullable Charset barcodeEncoding,
final Integer headerColor,
final String barcodeId, final CatimaBarcode barcodeType, final Integer headerColor,
final int starStatus, final Long lastUsed, final int archiveStatus) {
database.beginTransaction();
@@ -422,7 +411,6 @@ public class DBHelper extends SQLiteOpenHelper {
contentValues.put(LoyaltyCardDbIds.CARD_ID, cardId);
contentValues.put(LoyaltyCardDbIds.BARCODE_ID, barcodeId);
contentValues.put(LoyaltyCardDbIds.BARCODE_TYPE, barcodeType != null ? barcodeType.name() : null);
contentValues.put(LoyaltyCardDbIds.BARCODE_ENCODING, barcodeEncoding != null ? barcodeEncoding.name() : null);
contentValues.put(LoyaltyCardDbIds.HEADER_COLOR, headerColor);
contentValues.put(LoyaltyCardDbIds.STAR_STATUS, starStatus);
contentValues.put(LoyaltyCardDbIds.LAST_USED, lastUsed != null ? lastUsed : Utils.getUnixTime());
@@ -442,8 +430,7 @@ public class DBHelper extends SQLiteOpenHelper {
final SQLiteDatabase database, final int id, final String store, final String note,
final Date validFrom, final Date expiry, final BigDecimal balance,
final Currency balanceType, final String cardId, final String barcodeId,
final CatimaBarcode barcodeType, final @Nullable Charset barcodeEncoding,
final Integer headerColor, final int starStatus,
final CatimaBarcode barcodeType, final Integer headerColor, final int starStatus,
final Long lastUsed, final int archiveStatus) {
database.beginTransaction();
@@ -459,7 +446,6 @@ public class DBHelper extends SQLiteOpenHelper {
contentValues.put(LoyaltyCardDbIds.CARD_ID, cardId);
contentValues.put(LoyaltyCardDbIds.BARCODE_ID, barcodeId);
contentValues.put(LoyaltyCardDbIds.BARCODE_TYPE, barcodeType != null ? barcodeType.name() : null);
contentValues.put(LoyaltyCardDbIds.BARCODE_ENCODING, barcodeEncoding != null ? barcodeEncoding.name() : null);
contentValues.put(LoyaltyCardDbIds.HEADER_COLOR, headerColor);
contentValues.put(LoyaltyCardDbIds.STAR_STATUS, starStatus);
contentValues.put(LoyaltyCardDbIds.LAST_USED, lastUsed != null ? lastUsed : Utils.getUnixTime());
@@ -479,8 +465,7 @@ public class DBHelper extends SQLiteOpenHelper {
SQLiteDatabase database, final int id, final String store, final String note,
final Date validFrom, final Date expiry, final BigDecimal balance,
final Currency balanceType, final String cardId, final String barcodeId,
final CatimaBarcode barcodeType, final @Nullable Charset barcodeEncoding,
final Integer headerColor, final int starStatus,
final CatimaBarcode barcodeType, final Integer headerColor, final int starStatus,
final Long lastUsed, final int archiveStatus) {
database.beginTransaction();
@@ -495,7 +480,6 @@ public class DBHelper extends SQLiteOpenHelper {
contentValues.put(LoyaltyCardDbIds.CARD_ID, cardId);
contentValues.put(LoyaltyCardDbIds.BARCODE_ID, barcodeId);
contentValues.put(LoyaltyCardDbIds.BARCODE_TYPE, barcodeType != null ? barcodeType.name() : null);
contentValues.put(LoyaltyCardDbIds.BARCODE_ENCODING, barcodeEncoding != null ? barcodeEncoding.name() : null);
contentValues.put(LoyaltyCardDbIds.HEADER_COLOR, headerColor);
contentValues.put(LoyaltyCardDbIds.STAR_STATUS, starStatus);
contentValues.put(LoyaltyCardDbIds.LAST_USED, lastUsed != null ? lastUsed : Utils.getUnixTime());

View File

@@ -0,0 +1,402 @@
package protect.card_locker;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.InputType;
import android.util.Log;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.textfield.TextInputLayout;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import protect.card_locker.async.TaskHandler;
import protect.card_locker.databinding.ImportExportActivityBinding;
import protect.card_locker.importexport.DataFormat;
import protect.card_locker.importexport.ImportExportResult;
import protect.card_locker.importexport.ImportExportResultType;
public class ImportExportActivity extends CatimaAppCompatActivity {
private ImportExportActivityBinding binding;
private static final String TAG = "Catima";
private ImportExportTask importExporter;
private String importAlertTitle;
private String importAlertMessage;
private DataFormat importDataFormat;
private String exportPassword;
private ActivityResultLauncher<Intent> fileCreateLauncher;
private ActivityResultLauncher<String> fileOpenLauncher;
final private TaskHandler mTasks = new TaskHandler();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ImportExportActivityBinding.inflate(getLayoutInflater());
setTitle(R.string.importExport);
setContentView(binding.getRoot());
Utils.applyWindowInsets(binding.getRoot());
Toolbar toolbar = binding.toolbar;
setSupportActionBar(toolbar);
enableToolbarBackButton();
Intent fileIntent = getIntent();
if (fileIntent != null && fileIntent.getType() != null) {
chooseImportType(fileIntent.getData());
}
// would use ActivityResultContracts.CreateDocument() but mime type cannot be set
fileCreateLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
Intent intent = result.getData();
if (intent == null) {
Log.e(TAG, "Activity returned NULL data");
return;
}
Uri uri = intent.getData();
if (uri == null) {
Log.e(TAG, "Activity returned NULL uri");
return;
}
// Running this in a thread prevents Android from throwing a NetworkOnMainThreadException for large files
// FIXME: This is still suboptimal, because showing that the export started is delayed until the network request finishes
new Thread() {
@Override
public void run() {
try {
OutputStream writer = getContentResolver().openOutputStream(uri);
Log.d(TAG, "Starting file export with: " + result);
startExport(writer, uri, exportPassword.toCharArray(), true);
} catch (IOException e) {
Log.e(TAG, "Failed to export file: " + result, e);
onExportComplete(new ImportExportResult(ImportExportResultType.GenericFailure, result.toString()), uri);
}
}
}.start();
});
fileOpenLauncher = registerForActivityResult(new ActivityResultContracts.GetContent(), result -> {
if (result == null) {
Log.e(TAG, "Activity returned NULL data");
return;
}
openFileForImport(result, null);
});
// Check that there is a file manager available
final Intent intentCreateDocumentAction = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intentCreateDocumentAction.addCategory(Intent.CATEGORY_OPENABLE);
intentCreateDocumentAction.setType("application/zip");
intentCreateDocumentAction.putExtra(Intent.EXTRA_TITLE, "catima.zip");
Button exportButton = binding.exportButton;
exportButton.setOnClickListener(v -> {
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(ImportExportActivity.this);
builder.setTitle(R.string.exportPassword);
FrameLayout container = new FrameLayout(ImportExportActivity.this);
final TextInputLayout textInputLayout = new TextInputLayout(ImportExportActivity.this);
textInputLayout.setEndIconMode(TextInputLayout.END_ICON_PASSWORD_TOGGLE);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.setMargins(50, 10, 50, 0);
textInputLayout.setLayoutParams(params);
final EditText input = new EditText(ImportExportActivity.this);
input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
input.setHint(R.string.exportPasswordHint);
textInputLayout.addView(input);
container.addView(textInputLayout);
builder.setView(container);
builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
exportPassword = input.getText().toString();
try {
fileCreateLauncher.launch(intentCreateDocumentAction);
} catch (ActivityNotFoundException e) {
Toast.makeText(getApplicationContext(), R.string.failedOpeningFileManager, Toast.LENGTH_LONG).show();
Log.e(TAG, "No activity found to handle intent", e);
}
});
builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> dialogInterface.cancel());
builder.show();
});
// Check that there is a file manager available
Button importFilesystem = binding.importOptionFilesystemButton;
importFilesystem.setOnClickListener(v -> chooseImportType(null));
// FIXME: The importer/exporter is currently quite broken
// To prevent the screen from turning off during import/export and some devices killing Catima as it's no longer foregrounded, force the screen to stay on here
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
private void openFileForImport(Uri uri, char[] password) {
// Running this in a thread prevents Android from throwing a NetworkOnMainThreadException for large files
// FIXME: This is still suboptimal, because showing that the import started is delayed until the network request finishes
new Thread() {
@Override
public void run() {
try {
InputStream reader = getContentResolver().openInputStream(uri);
Log.d(TAG, "Starting file import with: " + uri);
startImport(reader, uri, importDataFormat, password, true);
} catch (IOException e) {
Log.e(TAG, "Failed to import file: " + uri, e);
onImportComplete(new ImportExportResult(ImportExportResultType.GenericFailure, e.toString()), uri, importDataFormat);
}
}
}.start();
}
private void chooseImportType(@Nullable Uri fileData) {
List<CharSequence> betaImportOptions = new ArrayList<>();
betaImportOptions.add("Fidme");
betaImportOptions.add("Stocard");
List<CharSequence> importOptions = new ArrayList<>();
for (String importOption : getResources().getStringArray(R.array.import_types_array)) {
if (betaImportOptions.contains(importOption)) {
importOption = importOption + " (BETA)";
}
importOptions.add(importOption);
}
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(R.string.chooseImportType)
.setItems(importOptions.toArray(new CharSequence[importOptions.size()]), (dialog, which) -> {
switch (which) {
// Catima
case 0:
importAlertTitle = getString(R.string.importCatima);
importAlertMessage = getString(R.string.importCatimaMessage);
importDataFormat = DataFormat.Catima;
break;
// Fidme
case 1:
importAlertTitle = getString(R.string.importFidme);
importAlertMessage = getString(R.string.importFidmeMessage);
importDataFormat = DataFormat.Fidme;
break;
// Loyalty Card Keychain
case 2:
importAlertTitle = getString(R.string.importLoyaltyCardKeychain);
importAlertMessage = getString(R.string.importLoyaltyCardKeychainMessage);
importDataFormat = DataFormat.Catima;
break;
// Stocard
case 3:
importAlertTitle = getString(R.string.importStocard);
importAlertMessage = getString(R.string.importStocardMessage);
importDataFormat = DataFormat.Stocard;
break;
// Voucher Vault
case 4:
importAlertTitle = getString(R.string.importVoucherVault);
importAlertMessage = getString(R.string.importVoucherVaultMessage);
importDataFormat = DataFormat.VoucherVault;
break;
default:
throw new IllegalArgumentException("Unknown DataFormat");
}
if (fileData != null) {
openFileForImport(fileData, null);
return;
}
new MaterialAlertDialogBuilder(this)
.setTitle(importAlertTitle)
.setMessage(importAlertMessage)
.setPositiveButton(R.string.ok, (dialog1, which1) -> {
try {
fileOpenLauncher.launch("*/*");
} catch (ActivityNotFoundException e) {
Toast.makeText(getApplicationContext(), R.string.failedOpeningFileManager, Toast.LENGTH_LONG).show();
Log.e(TAG, "No activity found to handle intent", e);
}
})
.setNegativeButton(R.string.cancel, null)
.show();
});
builder.show();
}
private void startImport(final InputStream target, final Uri targetUri, final DataFormat dataFormat, final char[] password, final boolean closeWhenDone) {
mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, true, false, false);
ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() {
@Override
public void onTaskComplete(ImportExportResult result, DataFormat dataFormat) {
onImportComplete(result, targetUri, dataFormat);
if (closeWhenDone) {
try {
target.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
};
importExporter = new ImportExportTask(ImportExportActivity.this,
dataFormat, target, password, listener);
mTasks.executeTask(TaskHandler.TYPE.IMPORT, importExporter);
}
private void startExport(final OutputStream target, final Uri targetUri, char[] password, final boolean closeWhenDone) {
mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, true, false, false);
ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() {
@Override
public void onTaskComplete(ImportExportResult result, DataFormat dataFormat) {
onExportComplete(result, targetUri);
if (closeWhenDone) {
try {
target.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
};
importExporter = new ImportExportTask(ImportExportActivity.this,
DataFormat.Catima, target, password, listener);
mTasks.executeTask(TaskHandler.TYPE.EXPORT, importExporter);
}
@Override
protected void onDestroy() {
mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, true, false, false);
mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, true, false, false);
super.onDestroy();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
private void retryWithPassword(DataFormat dataFormat, Uri uri) {
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(R.string.passwordRequired);
FrameLayout container = new FrameLayout(ImportExportActivity.this);
final TextInputLayout textInputLayout = new TextInputLayout(ImportExportActivity.this);
textInputLayout.setEndIconMode(TextInputLayout.END_ICON_PASSWORD_TOGGLE);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.setMargins(50, 10, 50, 0);
textInputLayout.setLayoutParams(params);
final EditText input = new EditText(ImportExportActivity.this);
input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
input.setHint(R.string.exportPasswordHint);
textInputLayout.addView(input);
container.addView(textInputLayout);
builder.setView(container);
builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
openFileForImport(uri, input.getText().toString().toCharArray());
});
builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> dialogInterface.cancel());
builder.show();
}
private String buildResultDialogMessage(ImportExportResult result, boolean isImport) {
int messageId;
if (result.resultType() == ImportExportResultType.Success) {
messageId = isImport ? R.string.importSuccessful : R.string.exportSuccessful;
} else {
messageId = isImport ? R.string.importFailed : R.string.exportFailed;
}
StringBuilder messageBuilder = new StringBuilder(getResources().getString(messageId));
if (result.developerDetails() != null) {
messageBuilder.append("\n\n");
messageBuilder.append(getResources().getString(R.string.include_if_asking_support));
messageBuilder.append("\n\n");
messageBuilder.append(result.developerDetails());
}
return messageBuilder.toString();
}
private void onImportComplete(ImportExportResult result, Uri path, DataFormat dataFormat) {
ImportExportResultType resultType = result.resultType();
if (resultType == ImportExportResultType.BadPassword) {
retryWithPassword(dataFormat, path);
return;
}
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(resultType == ImportExportResultType.Success ? R.string.importSuccessfulTitle : R.string.importFailedTitle);
builder.setMessage(buildResultDialogMessage(result, true));
builder.setNeutralButton(R.string.ok, (dialog, which) -> dialog.dismiss());
builder.create().show();
}
private void onExportComplete(ImportExportResult result, final Uri path) {
ImportExportResultType resultType = result.resultType();
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(resultType == ImportExportResultType.Success ? R.string.exportSuccessfulTitle : R.string.exportFailedTitle);
builder.setMessage(buildResultDialogMessage(result, false));
builder.setNeutralButton(R.string.ok, (dialog, which) -> dialog.dismiss());
if (resultType == ImportExportResultType.Success) {
final CharSequence sendLabel = ImportExportActivity.this.getResources().getText(R.string.sendLabel);
builder.setPositiveButton(sendLabel, (dialog, which) -> {
Intent sendIntent = new Intent(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_STREAM, path);
sendIntent.setType("text/csv");
// set flag to give temporary permission to external app to use the FileProvider
sendIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
ImportExportActivity.this.startActivity(Intent.createChooser(sendIntent,
sendLabel));
dialog.dismiss();
});
}
builder.create().show();
}
}

View File

@@ -1,416 +0,0 @@
package protect.card_locker
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.InputType
import android.util.Log
import android.view.MenuItem
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.Button
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.Toolbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputLayout
import protect.card_locker.async.TaskHandler
import protect.card_locker.databinding.ImportExportActivityBinding
import protect.card_locker.importexport.DataFormat
import protect.card_locker.importexport.ImportExportResult
import protect.card_locker.importexport.ImportExportResultType
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
class ImportExportActivity : CatimaAppCompatActivity() {
private lateinit var binding: ImportExportActivityBinding
private var importExporter: ImportExportTask? = null
private var importAlertTitle: String? = null
private var importAlertMessage: String? = null
private var importDataFormat: DataFormat? = null
private var exportPassword: String? = null
private lateinit var fileCreateLauncher: ActivityResultLauncher<Intent>
private lateinit var fileOpenLauncher: ActivityResultLauncher<String>
private val mTasks = TaskHandler()
companion object {
private const val TAG = "Catima"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ImportExportActivityBinding.inflate(layoutInflater)
setTitle(R.string.importExport)
setContentView(binding.root)
Utils.applyWindowInsets(binding.root)
val toolbar: Toolbar = binding.toolbar
setSupportActionBar(toolbar)
enableToolbarBackButton()
val fileIntent = intent
if (fileIntent?.type != null) {
chooseImportType(fileIntent.data)
}
// would use ActivityResultContracts.CreateDocument() but mime type cannot be set
fileCreateLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val intent = result.data
if (intent == null) {
Log.e(TAG, "Activity returned NULL data")
return@registerForActivityResult
}
val uri = intent.data
if (uri == null) {
Log.e(TAG, "Activity returned NULL uri")
return@registerForActivityResult
}
// Running this in a thread prevents Android from throwing a NetworkOnMainThreadException for large files
// FIXME: This is still suboptimal, because showing that the export started is delayed until the network request finishes
Thread {
try {
val writer = contentResolver.openOutputStream(uri)
Log.d(TAG, "Starting file export with: $result")
startExport(writer, uri, exportPassword?.toCharArray(), true)
} catch (e: IOException) {
Log.e(TAG, "Failed to export file: $result", e)
onExportComplete(
ImportExportResult(
ImportExportResultType.GenericFailure,
result.toString()
), uri
)
}
}.start()
}
fileOpenLauncher =
registerForActivityResult(ActivityResultContracts.GetContent()) { result ->
if (result == null) {
Log.e(TAG, "Activity returned NULL data")
return@registerForActivityResult
}
openFileForImport(result, null)
}
// Check that there is a file manager available
val intentCreateDocumentAction = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/zip"
putExtra(Intent.EXTRA_TITLE, "catima.zip")
}
val exportButton: Button = binding.exportButton
exportButton.setOnClickListener {
val builder = MaterialAlertDialogBuilder(this@ImportExportActivity)
builder.setTitle(R.string.exportPassword)
val container = FrameLayout(this@ImportExportActivity)
val textInputLayout = TextInputLayout(this@ImportExportActivity).apply {
endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
setMargins(50, 10, 50, 0)
}
}
val input = EditText(this@ImportExportActivity).apply {
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
setHint(R.string.exportPasswordHint)
}
textInputLayout.addView(input)
container.addView(textInputLayout)
builder.setView(container)
builder.setPositiveButton(R.string.ok) { _, _ ->
exportPassword = input.text.toString()
try {
fileCreateLauncher.launch(intentCreateDocumentAction)
} catch (e: ActivityNotFoundException) {
Toast.makeText(
applicationContext,
R.string.failedOpeningFileManager,
Toast.LENGTH_LONG
).show()
Log.e(TAG, "No activity found to handle intent", e)
}
}
builder.setNegativeButton(R.string.cancel) { dialogInterface, _ -> dialogInterface.cancel() }
builder.show()
}
// Check that there is a file manager available
val importFilesystem: Button = binding.importOptionFilesystemButton
importFilesystem.setOnClickListener { chooseImportType(null) }
// FIXME: The importer/exporter is currently quite broken
// To prevent the screen from turning off during import/export and some devices killing Catima as it's no longer foregrounded, force the screen to stay on here
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
private fun openFileForImport(uri: Uri, password: CharArray?) {
// Running this in a thread prevents Android from throwing a NetworkOnMainThreadException for large files
// FIXME: This is still suboptimal, because showing that the import started is delayed until the network request finishes
Thread {
try {
val reader = contentResolver.openInputStream(uri)
Log.d(TAG, "Starting file import with: $uri")
startImport(reader, uri, importDataFormat, password, true)
} catch (e: IOException) {
Log.e(TAG, "Failed to import file: $uri", e)
onImportComplete(
ImportExportResult(
ImportExportResultType.GenericFailure,
e.toString()
), uri, importDataFormat
)
}
}.start()
}
private fun chooseImportType(fileData: Uri?) {
val betaImportOptions = mutableListOf<CharSequence>()
betaImportOptions.add("Fidme")
val importOptions = mutableListOf<CharSequence>()
for (importOption in resources.getStringArray(R.array.import_types_array)) {
var option = importOption
if (betaImportOptions.contains(importOption)) {
option = "$importOption (BETA)"
}
importOptions.add(option)
}
val builder = MaterialAlertDialogBuilder(this)
builder.setTitle(R.string.chooseImportType)
.setItems(importOptions.toTypedArray()) { _, which ->
when (which) {
// Catima
0 -> {
importAlertTitle = getString(R.string.importCatima)
importAlertMessage = getString(R.string.importCatimaMessage)
importDataFormat = DataFormat.Catima
}
// Fidme
1 -> {
importAlertTitle = getString(R.string.importFidme)
importAlertMessage = getString(R.string.importFidmeMessage)
importDataFormat = DataFormat.Fidme
}
// Loyalty Card Keychain
2 -> {
importAlertTitle = getString(R.string.importLoyaltyCardKeychain)
importAlertMessage = getString(R.string.importLoyaltyCardKeychainMessage)
importDataFormat = DataFormat.Catima
}
// Voucher Vault
3 -> {
importAlertTitle = getString(R.string.importVoucherVault)
importAlertMessage = getString(R.string.importVoucherVaultMessage)
importDataFormat = DataFormat.VoucherVault
}
else -> throw IllegalArgumentException("Unknown DataFormat")
}
if (fileData != null) {
openFileForImport(fileData, null)
return@setItems
}
MaterialAlertDialogBuilder(this)
.setTitle(importAlertTitle)
.setMessage(importAlertMessage)
.setPositiveButton(R.string.ok) { _, _ ->
try {
fileOpenLauncher.launch("*/*")
} catch (e: ActivityNotFoundException) {
Toast.makeText(
applicationContext,
R.string.failedOpeningFileManager,
Toast.LENGTH_LONG
).show()
Log.e(TAG, "No activity found to handle intent", e)
}
}
.setNegativeButton(R.string.cancel, null)
.show()
}
builder.show()
}
private fun startImport(
target: InputStream?,
targetUri: Uri,
dataFormat: DataFormat?,
password: CharArray?,
closeWhenDone: Boolean
) {
mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, true, false, false)
val listener = ImportExportTask.TaskCompleteListener { result, dataFormat ->
onImportComplete(result, targetUri, dataFormat)
if (closeWhenDone) {
try {
target?.close()
} catch (ioException: IOException) {
ioException.printStackTrace()
}
}
}
importExporter = ImportExportTask(
this@ImportExportActivity,
dataFormat, target, password, listener
)
mTasks.executeTask(TaskHandler.TYPE.IMPORT, importExporter)
}
private fun startExport(
target: OutputStream?,
targetUri: Uri,
password: CharArray?,
closeWhenDone: Boolean
) {
mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, true, false, false)
val listener = ImportExportTask.TaskCompleteListener { result, dataFormat ->
onExportComplete(result, targetUri)
if (closeWhenDone) {
try {
target?.close()
} catch (ioException: IOException) {
ioException.printStackTrace()
}
}
}
importExporter = ImportExportTask(
this@ImportExportActivity,
DataFormat.Catima, target, password, listener
)
mTasks.executeTask(TaskHandler.TYPE.EXPORT, importExporter)
}
override fun onDestroy() {
mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, true, false, false)
mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, true, false, false)
super.onDestroy()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val id = item.itemId
if (id == android.R.id.home) {
finish()
return true
}
return super.onOptionsItemSelected(item)
}
private fun retryWithPassword(dataFormat: DataFormat, uri: Uri) {
val builder = MaterialAlertDialogBuilder(this)
builder.setTitle(R.string.passwordRequired)
val container = FrameLayout(this@ImportExportActivity)
val textInputLayout = TextInputLayout(this@ImportExportActivity).apply {
endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
setMargins(50, 10, 50, 0)
}
}
val input = EditText(this@ImportExportActivity).apply {
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
setHint(R.string.exportPasswordHint)
}
textInputLayout.addView(input)
container.addView(textInputLayout)
builder.setView(container)
builder.setPositiveButton(R.string.ok) { _, _ ->
openFileForImport(uri, input.text.toString().toCharArray())
}
builder.setNegativeButton(R.string.cancel) { dialogInterface, _ -> dialogInterface.cancel() }
builder.show()
}
private fun buildResultDialogMessage(result: ImportExportResult, isImport: Boolean): String {
val messageId = if (result.resultType() == ImportExportResultType.Success) {
if (isImport) R.string.importSuccessful else R.string.exportSuccessful
} else {
if (isImport) R.string.importFailed else R.string.exportFailed
}
val messageBuilder = StringBuilder(resources.getString(messageId))
if (result.developerDetails() != null) {
messageBuilder.append("\n\n")
messageBuilder.append(resources.getString(R.string.include_if_asking_support))
messageBuilder.append("\n\n")
messageBuilder.append(result.developerDetails())
}
return messageBuilder.toString()
}
private fun onImportComplete(result: ImportExportResult, path: Uri, dataFormat: DataFormat?) {
val resultType = result.resultType()
if (resultType == ImportExportResultType.BadPassword) {
retryWithPassword(dataFormat!!, path)
return
}
val builder = MaterialAlertDialogBuilder(this)
builder.setTitle(if (resultType == ImportExportResultType.Success) R.string.importSuccessfulTitle else R.string.importFailedTitle)
builder.setMessage(buildResultDialogMessage(result, true))
builder.setNeutralButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
builder.create().show()
}
private fun onExportComplete(result: ImportExportResult, path: Uri) {
val resultType = result.resultType()
val builder = MaterialAlertDialogBuilder(this)
builder.setTitle(if (resultType == ImportExportResultType.Success) R.string.exportSuccessfulTitle else R.string.exportFailedTitle)
builder.setMessage(buildResultDialogMessage(result, false))
builder.setNeutralButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
if (resultType == ImportExportResultType.Success) {
val sendLabel = this@ImportExportActivity.resources.getText(R.string.sendLabel)
builder.setPositiveButton(sendLabel) { dialog, _ ->
val sendIntent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, path)
type = "text/csv"
// set flag to give temporary permission to external app to use the FileProvider
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
this@ImportExportActivity.startActivity(Intent.createChooser(sendIntent, sendLabel))
dialog.dismiss()
}
}
builder.create().show()
}
}

View File

@@ -1,17 +1,12 @@
package protect.card_locker;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import android.widget.Toast;
import java.io.IOException;
import java.io.InputStream;
@@ -37,7 +32,7 @@ public class ImportExportTask implements CompatCallable<ImportExportResult> {
private char[] password;
private TaskCompleteListener listener;
private AlertDialog progress;
private ProgressDialog progress;
/**
* Constructor which will setup a task for exporting to the given file
@@ -93,36 +88,12 @@ public class ImportExportTask implements CompatCallable<ImportExportResult> {
}
public void onPreExecute() {
MaterialAlertDialogBuilder progressDialogBuilder = new MaterialAlertDialogBuilder(activity);
progressDialogBuilder.setCancelable(false); // Don't cancel if user taps next to dialog
progressDialogBuilder.setTitle(doImport ? R.string.importing : R.string.exporting);
progress = new ProgressDialog(activity);
progress.setTitle(doImport ? R.string.importing : R.string.exporting);
// Create components
TextView progressDialogTextView = new TextView(activity);
progressDialogTextView.setText(R.string.pleaseDoNotRotateTheDevice); // FIXME: Instead of telling the user to not rotate, rotation should not cancel the import
ProgressBar progressDialogProgressBar = new ProgressBar(activity);
progressDialogProgressBar.setIndeterminate(true);
progress.setOnCancelListener(dialog -> cancel());
progress.setOnDismissListener(dialog -> cancel());
// Create LinearLayout (to put the components below each other)
LinearLayout progressDialogLayout = new LinearLayout(activity);
progressDialogLayout.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams progressDialogLayoutParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
);
int contentPadding = activity.getResources().getDimensionPixelSize(R.dimen.alert_dialog_content_padding);
progressDialogLayoutParams.setMargins(contentPadding, contentPadding / 2, contentPadding, 0);
// Put components in layout
progressDialogLayout.addView(progressDialogTextView, progressDialogLayoutParams);
progressDialogLayout.addView(progressDialogProgressBar, progressDialogLayoutParams);
// Create and show dialog
progressDialogBuilder.setView(progressDialogLayout);
progressDialogBuilder.setNeutralButton(R.string.cancel, (dialogInterface, i) -> cancel());
progressDialogBuilder.setOnCancelListener(dialogInterface -> cancel());
progressDialogBuilder.setOnDismissListener(dialogInterface -> cancel());
progress = progressDialogBuilder.create();
progress.show();
}

View File

@@ -4,14 +4,11 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.Nullable;
import java.io.InvalidObjectException;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Currency;
import java.util.Date;
@@ -28,7 +25,6 @@ public class ImportURIHelper {
private static final String CARD_ID = DBHelper.LoyaltyCardDbIds.CARD_ID;
private static final String BARCODE_ID = DBHelper.LoyaltyCardDbIds.BARCODE_ID;
private static final String BARCODE_TYPE = DBHelper.LoyaltyCardDbIds.BARCODE_TYPE;
private static final String BARCODE_ENCODING = DBHelper.LoyaltyCardDbIds.BARCODE_ENCODING;
private static final String HEADER_COLOR = DBHelper.LoyaltyCardDbIds.HEADER_COLOR;
private final Context context;
@@ -70,7 +66,6 @@ public class ImportURIHelper {
try {
// These values are allowed to be null
CatimaBarcode barcodeType = null;
Charset barcodeEncoding = null;
Date validFrom = null;
Date expiry = null;
BigDecimal balance = new BigDecimal("0");
@@ -108,11 +103,6 @@ public class ImportURIHelper {
barcodeType = CatimaBarcode.fromName(unparsedBarcodeType);
}
String unparsedBarcodeEncoding = kv.get(BARCODE_ENCODING);
if (unparsedBarcodeEncoding != null && !unparsedBarcodeEncoding.equals("")) {
barcodeEncoding = Charset.forName(unparsedBarcodeEncoding);
}
String unparsedBalance = kv.get(BALANCE);
if (unparsedBalance != null && !unparsedBalance.equals("")) {
balance = new BigDecimal(unparsedBalance);
@@ -146,7 +136,6 @@ public class ImportURIHelper {
cardId,
barcodeId,
barcodeType,
barcodeEncoding,
headerColor,
0,
Utils.getUnixTime(),
@@ -206,9 +195,6 @@ public class ImportURIHelper {
if (loyaltyCard.barcodeType != null) {
fragment = appendFragment(fragment, BARCODE_TYPE, loyaltyCard.barcodeType.name());
}
if (loyaltyCard.barcodeEncoding != null) {
fragment = appendFragment(fragment, BARCODE_ENCODING, loyaltyCard.barcodeEncoding.name());
}
if (loyaltyCard.headerColor != null) {
fragment = appendFragment(fragment, HEADER_COLOR, loyaltyCard.headerColor.toString());
}

View File

@@ -0,0 +1,145 @@
package protect.card_locker;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.text.TextPaint;
import android.util.Log;
import androidx.core.graphics.PaintCompat;
/**
* Original from https://github.com/andOTP/andOTP/blob/master/app/src/main/java/org/shadowice/flocke/andotp/Utilities/LetterBitmap.java
* which was originally from http://stackoverflow.com/questions/23122088/colored-boxed-with-letters-a-la-gmail
* Used to create a {@link Bitmap} that contains a letter used in the English
* alphabet or digit, if there is no letter or digit available, a default image
* is shown instead.
*/
class LetterBitmap {
/**
* The number of available tile colors
*/
private static final int NUM_OF_TILE_COLORS = 8;
/**
* The letter bitmap
*/
private final Bitmap mBitmap;
/**
* The background color of the letter bitmap
*/
private final Integer mColor;
/**
* Constructor for <code>LetterTileProvider</code>
*
* @param context The {@link Context} to use
* @param displayName The name used to create the letter for the tile
* @param key The key used to generate the background color for the tile
* @param tileLetterFontSize The font size used to display the letter
* @param width The desired width of the tile
* @param height The desired height of the tile
* @param backgroundColor (optional) color to use for background.
* @param textColor (optional) color to use for text.
*/
public LetterBitmap(Context context, String displayName, String key, int tileLetterFontSize,
int width, int height, Integer backgroundColor, Integer textColor) {
TextPaint paint = new TextPaint();
if (textColor != null) {
paint.setColor(textColor);
} else {
paint.setColor(Color.WHITE);
}
paint.setTextAlign(Paint.Align.CENTER);
paint.setAntiAlias(true);
paint.setTextSize(tileLetterFontSize);
paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
if (backgroundColor == null) {
mColor = getDefaultColor(context, key);
} else {
mColor = backgroundColor;
}
mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
String firstChar = displayName.substring(0, 1).toUpperCase();
int firstCharEnd = 2;
while (firstCharEnd <= displayName.length()) {
// Test for the longest render-able string
// But ignore containing only a-Z0-9 to not render things like ffi as a single character
String test = displayName.substring(0, firstCharEnd);
if (!isAlphabetical(test) && PaintCompat.hasGlyph(paint, test)) {
firstChar = test;
}
firstCharEnd++;
}
Log.d("LetterBitmap", "using sequence " + firstChar + " to render first char which has length " + firstChar.length());
final Canvas c = new Canvas();
c.setBitmap(mBitmap);
c.drawColor(mColor);
Rect bounds = new Rect();
paint.getTextBounds(firstChar, 0, firstChar.length(), bounds);
c.drawText(firstChar,
0, firstChar.length(),
width / 2.0f, (height - (bounds.bottom + bounds.top)) / 2.0f
, paint);
}
/**
* @return A {@link Bitmap} that contains a letter used in the English
* alphabet or digit, if there is no letter or digit available, a
* default image is shown instead
*/
public Bitmap getLetterTile() {
return mBitmap;
}
/**
* @return background color used for letter title.
*/
public int getBackgroundColor() {
return mColor;
}
/**
* @param key The key used to generate the tile color
* @return A new or previously chosen color for <code>key</code> used as the
* tile background color
*/
private static int pickColor(String key, TypedArray colors) {
// String.hashCode() is not supposed to change across java versions, so
// this should guarantee the same key always maps to the same color
final int color = Math.abs(key.hashCode()) % NUM_OF_TILE_COLORS;
return colors.getColor(color, Color.BLACK);
}
private static boolean isAlphabetical(String string) {
return string.matches("[a-zA-Z0-9]*");
}
/**
* Determine the color which the letter tile will use if no default
* color is provided.
*/
public static int getDefaultColor(Context context, String key) {
final Resources res = context.getResources();
TypedArray colors = res.obtainTypedArray(R.array.letter_tile_colors);
int color = pickColor(key, colors);
colors.recycle();
return color;
}
}

View File

@@ -1,136 +0,0 @@
package protect.card_locker
import android.content.Context
import android.content.res.TypedArray
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.Typeface
import android.text.TextPaint
import android.util.Log
import androidx.core.graphics.PaintCompat
import java.util.Locale
import kotlin.math.abs
/**
* Original from https://github.com/andOTP/andOTP/blob/master/app/src/main/java/org/shadowice/flocke/andotp/Utilities/LetterBitmap.java
* which was originally from http://stackoverflow.com/questions/23122088/colored-boxed-with-letters-a-la-gmail
* Used to create a {@link Bitmap} that contains a letter used in the English
* alphabet or digit, if there is no letter or digit available, a default image
* is shown instead.
*
* @constructor Constructor for <code>LetterTileProvider</code>
* @param context The {@link Context} to use
* @param displayName The name used to create the letter for the tile
* @param key The key used to generate the background color for the tile
* @param tileLetterFontSize The font size used to display the letter
* @param width The desired width of the tile
* @param height The desired height of the tile
* @param backgroundColor (optional) color to use for background.
* @param textColor (optional) color to use for text.
*/
class LetterBitmap(
context: Context, displayName: String, key: String, tileLetterFontSize: Int,
width: Int, height: Int, backgroundColor: Int?, textColor: Int?
) {
/**
* A {@link Bitmap} that contains a letter used in the English
* alphabet or digit, if there is no letter or digit available, a
* default image is shown instead
*/
private val letterTile: Bitmap
/**
* The background color of the letter bitmap
*/
private val mColor: Int
init {
val paint = TextPaint().apply {
color = textColor ?: Color.WHITE
textAlign = Paint.Align.CENTER
isAntiAlias = true
textSize = tileLetterFontSize.toFloat()
typeface = Typeface.defaultFromStyle(Typeface.BOLD)
}
mColor = backgroundColor ?: getDefaultColor(context, key)
this.letterTile = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
var firstChar = displayName.substring(0, 1).uppercase(Locale.getDefault())
var firstCharEnd = 2
while (firstCharEnd <= displayName.length) {
// Test for the longest render-able string
// But ignore containing only a-Z0-9 to not render things like ffi as a single character
val test = displayName.substring(0, firstCharEnd)
if (!isAlphabetical(test) && PaintCompat.hasGlyph(paint, test)) {
firstChar = test
}
firstCharEnd++
}
Log.d(
"LetterBitmap",
"using sequence $firstChar to render first char which has length ${firstChar.length}"
)
Canvas().apply {
setBitmap(this@LetterBitmap.letterTile)
drawColor(mColor)
val bounds = Rect()
paint.getTextBounds(firstChar, 0, firstChar.length, bounds)
drawText(
firstChar,
0, firstChar.length,
width / 2.0f, (height - (bounds.bottom + bounds.top)) / 2.0f,
paint
)
}
}
val backgroundColor: Int
/**
* @return background color used for letter title.
*/
get() = mColor
fun getLetterTile(): Bitmap {
return letterTile
}
companion object {
/**
* @param key The key used to generate the tile color
* @return A new or previously chosen color for `key` used as the
* tile background color
*/
private fun pickColor(key: String, colors: TypedArray): Int {
// String.hashCode() is not supposed to change across java versions, so
// this should guarantee the same key always maps to the same color
val color = abs(key.hashCode()) % colors.length()
return colors.getColor(color, Color.BLACK)
}
private fun isAlphabetical(string: String): Boolean {
return string.matches("[a-zA-Z0-9]*".toRegex())
}
/**
* Determine the color which the letter tile will use if no default
* color is provided.
*/
fun getDefaultColor(context: Context, key: String): Int {
val res = context.resources
val colors = res.obtainTypedArray(R.array.letter_tile_colors)
val color: Int = pickColor(key, colors)
colors.recycle()
return color
}
}
}

View File

@@ -101,8 +101,7 @@ class ListWidget : AppWidgetProvider() {
setInt(R.id.item_container_foreground, "setBackgroundColor", headerColor)
val icon = loyaltyCard.getImageThumbnail(context)
// setImageViewIcon is not supported on Android 5, so force Android 5 down the text path
// FIXME: The icon flow causes a crash up to Android 12L, so SDK_INT is forced up from 23 to 33
if (icon != null && Build.VERSION.SDK_INT >= 32) {
if (icon != null && Build.VERSION.SDK_INT >= 23) {
setInt(R.id.item_container_foreground, "setBackgroundColor", foreground)
setImageViewIcon(R.id.item_image, Icon.createWithBitmap(icon))
setViewVisibility(R.id.item_text, View.INVISIBLE)

View File

@@ -9,7 +9,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.util.Currency;
import java.util.Date;
import java.util.List;
@@ -32,8 +31,6 @@ public class LoyaltyCard {
@Nullable
public CatimaBarcode barcodeType;
@Nullable
public Charset barcodeEncoding;
@Nullable
public Integer headerColor;
public int starStatus;
public long lastUsed;
@@ -65,7 +62,6 @@ public class LoyaltyCard {
public static final String BUNDLE_LOYALTY_CARD_CARD_ID = "loyaltyCardCardId";
public static final String BUNDLE_LOYALTY_CARD_BARCODE_ID = "loyaltyCardBarcodeId";
public static final String BUNDLE_LOYALTY_CARD_BARCODE_TYPE = "loyaltyCardBarcodeType";
public static final String BUNDLE_LOYALTY_CARD_BARCODE_ENCODING = "loyaltyCardBarcodeEncoding";
public static final String BUNDLE_LOYALTY_CARD_HEADER_COLOR = "loyaltyCardHeaderColor";
public static final String BUNDLE_LOYALTY_CARD_STAR_STATUS = "loyaltyCardStarStatus";
public static final String BUNDLE_LOYALTY_CARD_LAST_USED = "loyaltyCardLastUsed";
@@ -94,7 +90,6 @@ public class LoyaltyCard {
setCardId("");
setBarcodeId(null);
setBarcodeType(null);
setBarcodeEncoding(null);
setHeaderColor(null);
setStarStatus(0);
setLastUsed(Utils.getUnixTime());
@@ -129,7 +124,7 @@ public class LoyaltyCard {
public LoyaltyCard(final int id, final String store, final String note, @Nullable final Date validFrom,
@Nullable final Date expiry, final BigDecimal balance, @Nullable final Currency balanceType,
final String cardId, @Nullable final String barcodeId, @Nullable final CatimaBarcode barcodeType,
@Nullable final Charset barcodeEncoding, @Nullable final Integer headerColor, final int starStatus,
@Nullable final Integer headerColor, final int starStatus,
final long lastUsed, final int zoomLevel, final int zoomLevelWidth, final int archiveStatus,
@Nullable Bitmap imageThumbnail, @Nullable String imageThumbnailPath,
@Nullable Bitmap imageFront, @Nullable String imageFrontPath,
@@ -144,7 +139,6 @@ public class LoyaltyCard {
setCardId(cardId);
setBarcodeId(barcodeId);
setBarcodeType(barcodeType);
setBarcodeEncoding(barcodeEncoding);
setHeaderColor(headerColor);
setStarStatus(starStatus);
setLastUsed(lastUsed);
@@ -250,10 +244,6 @@ public class LoyaltyCard {
this.barcodeType = barcodeType;
}
public void setBarcodeEncoding(@Nullable Charset barcodeEncoding) {
this.barcodeEncoding = barcodeEncoding;
}
public void setHeaderColor(@Nullable Integer headerColor) {
this.headerColor = headerColor;
}
@@ -389,11 +379,6 @@ public class LoyaltyCard {
} else if (requireFull) {
throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_BARCODE_TYPE);
}
if (bundle.containsKey(BUNDLE_LOYALTY_CARD_BARCODE_ENCODING)) {
setBarcodeEncoding(Charset.forName(bundle.getString(BUNDLE_LOYALTY_CARD_BARCODE_ENCODING)));
} else if (requireFull) {
throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_BARCODE_ENCODING);
}
if (bundle.containsKey(BUNDLE_LOYALTY_CARD_HEADER_COLOR)) {
int tmpHeaderColor = bundle.getInt(BUNDLE_LOYALTY_CARD_HEADER_COLOR);
setHeaderColor(tmpHeaderColor != -1 ? tmpHeaderColor : null);
@@ -477,9 +462,6 @@ public class LoyaltyCard {
if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_BARCODE_TYPE)) {
bundle.putString(BUNDLE_LOYALTY_CARD_BARCODE_TYPE, barcodeType != null ? barcodeType.name() : null);
}
if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_BARCODE_ENCODING)) {
bundle.putString(BUNDLE_LOYALTY_CARD_BARCODE_ENCODING, barcodeEncoding != null ? barcodeEncoding.name() : null);
}
if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_HEADER_COLOR)) {
bundle.putInt(BUNDLE_LOYALTY_CARD_HEADER_COLOR, headerColor != null ? headerColor : -1);
}
@@ -557,9 +539,6 @@ public class LoyaltyCard {
// barcodeType
int barcodeTypeColumn = cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BARCODE_TYPE);
CatimaBarcode barcodeType = !cursor.isNull(barcodeTypeColumn) ? CatimaBarcode.fromName(cursor.getString(barcodeTypeColumn)) : null;
// barcodeEncoding
int barcodeEncodingColumn = cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BARCODE_ENCODING);
Charset barcodeEncoding = !cursor.isNull(barcodeEncodingColumn) ? Charset.forName(cursor.getString(barcodeEncodingColumn)) : null;
// headerColor
int headerColorColumn = cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.HEADER_COLOR);
Integer headerColor = !cursor.isNull(headerColorColumn) ? cursor.getInt(headerColorColumn) : null;
@@ -585,7 +564,6 @@ public class LoyaltyCard {
cardId,
barcodeId,
barcodeType,
barcodeEncoding,
headerColor,
starStatus,
lastUsed,
@@ -615,7 +593,6 @@ public class LoyaltyCard {
Utils.equals(a.barcodeId, b.barcodeId) && // nullable String
Utils.equals(a.barcodeType == null ? null : a.barcodeType.format(),
b.barcodeType == null ? null : b.barcodeType.format()) && // nullable CatimaBarcode with no overridden .equals(), so we need to check .format()
Utils.equals(a.barcodeEncoding, b.barcodeEncoding) && // nullable String
Utils.equals(a.headerColor, b.headerColor) && // nullable Integer
a.starStatus == b.starStatus && // non-nullable int
a.archiveStatus == b.archiveStatus && // non-nullable int
@@ -642,7 +619,7 @@ public class LoyaltyCard {
public String toString() {
return String.format(
"LoyaltyCard{%n id=%s,%n store=%s,%n note=%s,%n validFrom=%s,%n expiry=%s,%n"
+ " balance=%s,%n balanceType=%s,%n cardId=%s,%n barcodeId=%s,%n barcodeType=%s,%n barcodeEncoding=%s,%n"
+ " balance=%s,%n balanceType=%s,%n cardId=%s,%n barcodeId=%s,%n barcodeType=%s,%n"
+ " headerColor=%s,%n starStatus=%s,%n lastUsed=%s,%n zoomLevel=%s,%n zoomLevelWidth=%s,%n archiveStatus=%s%n"
+ " imageThumbnail=%s,%n imageThumbnailPath=%s,%n imageFront=%s,%n imageFrontPath=%s,%n imageBack=%s,%n imageBackPath=%s,%n}",
this.id,
@@ -655,7 +632,6 @@ public class LoyaltyCard {
this.cardId,
this.barcodeId,
this.barcodeType != null ? this.barcodeType.format() : null,
this.barcodeEncoding != null ? this.barcodeEncoding.name() : null,
this.headerColor,
this.starStatus,
this.lastUsed,

View File

@@ -70,8 +70,6 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.ArrayList;
@@ -125,12 +123,11 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
ChipGroup groupsChips;
AutoCompleteTextView validFromField;
AutoCompleteTextView expiryField;
AutoCompleteTextView balanceCurrencyField;
EditText balanceField;
AutoCompleteTextView balanceCurrencyField;
TextView cardIdFieldView;
AutoCompleteTextView barcodeIdField;
AutoCompleteTextView barcodeTypeField;
AutoCompleteTextView barcodeEncodingField;
ImageView barcodeImage;
View barcodeImageLayout;
View barcodeCaptureLayout;
@@ -151,9 +148,9 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
boolean onRestoring = false;
AlertDialog confirmExitDialog = null;
boolean validBalance = true;
HashMap<String, Currency> currencies = new HashMap<>();
HashMap<String, String> currencySymbols = new HashMap<>();
boolean validBalance = true;
ActivityResultLauncher<Uri> mPhotoTakerLauncher;
ActivityResultLauncher<Intent> mPhotoPickerLauncher;
@@ -196,14 +193,14 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
viewModel.setHasChanged(true);
}
protected void setLoyaltyCardBalanceType(@Nullable Currency balanceType) {
viewModel.getLoyaltyCard().setBalanceType(balanceType);
protected void setLoyaltyCardBalance(@NonNull BigDecimal balance) {
viewModel.getLoyaltyCard().setBalance(balance);
viewModel.setHasChanged(true);
}
protected void setLoyaltyCardBalance(@NonNull BigDecimal balance) {
viewModel.getLoyaltyCard().setBalance(balance);
protected void setLoyaltyCardBalanceType(@Nullable Currency balanceType) {
viewModel.getLoyaltyCard().setBalanceType(balanceType);
viewModel.setHasChanged(true);
}
@@ -232,14 +229,6 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
viewModel.setHasChanged(true);
}
protected void setLoyaltyCardBarcodeEncoding(@Nullable Charset barcodeEncoding) {
viewModel.getLoyaltyCard().setBarcodeEncoding(barcodeEncoding);
generateBarcode();
viewModel.setHasChanged(true);
}
protected void setLoyaltyCardHeaderColor(@Nullable Integer headerColor) {
viewModel.getLoyaltyCard().setHeaderColor(headerColor);
@@ -340,12 +329,11 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
groupsChips = binding.groupChips;
validFromField = binding.validFromField;
expiryField = binding.expiryField;
balanceCurrencyField = binding.balanceCurrencyField;
balanceField = binding.balanceField;
balanceCurrencyField = binding.balanceCurrencyField;
cardIdFieldView = binding.cardIdView;
barcodeIdField = binding.barcodeIdField;
barcodeTypeField = binding.barcodeTypeField;
barcodeEncodingField = binding.barcodeEncodingField;
barcodeImage = binding.barcode;
barcodeImage.setClipToOutline(true);
barcodeImageLayout = binding.barcodeLayout;
@@ -385,6 +373,33 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
setMaterialDatePickerResultListener();
balanceField.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus && !onResuming && !onRestoring) {
if (balanceField.getText().toString().isEmpty()) {
setLoyaltyCardBalance(BigDecimal.valueOf(0));
}
balanceField.setText(Utils.formatBalanceWithoutCurrencySymbol(viewModel.getLoyaltyCard().balance, viewModel.getLoyaltyCard().balanceType));
}
});
balanceField.addTextChangedListener(new SimpleTextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (onResuming || onRestoring) return;
try {
BigDecimal balance = Utils.parseBalance(s.toString(), viewModel.getLoyaltyCard().balanceType);
setLoyaltyCardBalance(balance);
balanceField.setError(null);
validBalance = true;
} catch (ParseException e) {
e.printStackTrace();
balanceField.setError(getString(R.string.balanceParsingFailed));
validBalance = false;
}
}
});
balanceCurrencyField.addTextChangedListener(new SimpleTextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
@@ -437,33 +452,6 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
}
});
balanceField.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus && !onResuming && !onRestoring) {
if (balanceField.getText().toString().isEmpty()) {
setLoyaltyCardBalance(BigDecimal.valueOf(0));
}
balanceField.setText(Utils.formatBalanceWithoutCurrencySymbol(viewModel.getLoyaltyCard().balance, viewModel.getLoyaltyCard().balanceType));
}
});
balanceField.addTextChangedListener(new SimpleTextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (onResuming || onRestoring) return;
try {
BigDecimal balance = Utils.parseBalance(s.toString(), viewModel.getLoyaltyCard().balanceType);
setLoyaltyCardBalance(balance);
balanceField.setError(null);
validBalance = true;
} catch (ParseException e) {
e.printStackTrace();
balanceField.setError(getString(R.string.balanceParsingFailed));
validBalance = false;
}
}
});
cardIdFieldView.addTextChangedListener(new SimpleTextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
@@ -589,30 +577,6 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
}
});
barcodeEncodingField.addTextChangedListener(new SimpleTextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (!s.toString().isEmpty()) {
Log.d(TAG, "Setting barcode encoding to " + s.toString());
if (s.toString().equals(getString(R.string.automatic))) {
setLoyaltyCardBarcodeEncoding(null);
} else {
setLoyaltyCardBarcodeEncoding(Charset.forName(s.toString()));
}
}
}
@Override
public void afterTextChanged(Editable s) {
ArrayList<String> barcodeEncodingList = new ArrayList<>();
barcodeEncodingList.add(getString(R.string.automatic));
barcodeEncodingList.add(StandardCharsets.ISO_8859_1.name());
barcodeEncodingList.add(StandardCharsets.UTF_8.name());
ArrayAdapter<String> barcodeEncodingAdapter = new ArrayAdapter<>(LoyaltyCardEditActivity.this, android.R.layout.select_dialog_item, barcodeEncodingList);
barcodeEncodingField.setAdapter(barcodeEncodingAdapter);
}
});
binding.tabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
@@ -755,6 +719,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
int colorOnSurface = MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSurface, ContextCompat.getColor(this, R.color.md_theme_light_onSurface));
int colorBackground = MaterialColors.getColor(this, android.R.attr.colorBackground, ContextCompat.getColor(this, R.color.md_theme_light_onSurface));
mCropperOptions.setToolbarColor(colorSurface);
mCropperOptions.setStatusBarColor(colorSurface);
mCropperOptions.setToolbarWidgetColor(colorOnSurface);
mCropperOptions.setRootViewBackgroundColor(colorBackground);
// set tool tip to be the darker of primary color
@@ -809,8 +774,6 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
barcodeIdField.setText(barcodeId != null && !barcodeId.isEmpty() ? barcodeId : getString(R.string.sameAsCardId));
CatimaBarcode barcodeType = viewModel.getLoyaltyCard().barcodeType;
barcodeTypeField.setText(barcodeType != null ? barcodeType.prettyName() : getString(R.string.noBarcode));
Charset barcodeEncoding = viewModel.getLoyaltyCard().barcodeEncoding;
barcodeEncodingField.setText(barcodeEncoding != null ? barcodeEncoding.name() : getString(R.string.automatic));
// We set the balance here (with onResuming/onRestoring == true) to prevent formatBalanceCurrencyField() from setting it (via onTextChanged),
// which can cause issues when switching locale because it parses the balance and e.g. the decimal separator may have changed.
@@ -1517,9 +1480,9 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
// This makes the DBHelper set it to the current date
// So that new and edited card are always on top when sorting by recently used
if (viewModel.getUpdateLoyaltyCard()) {
DBHelper.updateLoyaltyCard(mDatabase, viewModel.getLoyaltyCardId(), viewModel.getLoyaltyCard().store, viewModel.getLoyaltyCard().note, viewModel.getLoyaltyCard().validFrom, viewModel.getLoyaltyCard().expiry, viewModel.getLoyaltyCard().balance, viewModel.getLoyaltyCard().balanceType, viewModel.getLoyaltyCard().cardId, viewModel.getLoyaltyCard().barcodeId, viewModel.getLoyaltyCard().barcodeType, viewModel.getLoyaltyCard().barcodeEncoding, viewModel.getLoyaltyCard().headerColor, viewModel.getLoyaltyCard().starStatus, null, viewModel.getLoyaltyCard().archiveStatus);
DBHelper.updateLoyaltyCard(mDatabase, viewModel.getLoyaltyCardId(), viewModel.getLoyaltyCard().store, viewModel.getLoyaltyCard().note, viewModel.getLoyaltyCard().validFrom, viewModel.getLoyaltyCard().expiry, viewModel.getLoyaltyCard().balance, viewModel.getLoyaltyCard().balanceType, viewModel.getLoyaltyCard().cardId, viewModel.getLoyaltyCard().barcodeId, viewModel.getLoyaltyCard().barcodeType, viewModel.getLoyaltyCard().headerColor, viewModel.getLoyaltyCard().starStatus, null, viewModel.getLoyaltyCard().archiveStatus);
} else {
viewModel.setLoyaltyCardId((int) DBHelper.insertLoyaltyCard(mDatabase, viewModel.getLoyaltyCard().store, viewModel.getLoyaltyCard().note, viewModel.getLoyaltyCard().validFrom, viewModel.getLoyaltyCard().expiry, viewModel.getLoyaltyCard().balance, viewModel.getLoyaltyCard().balanceType, viewModel.getLoyaltyCard().cardId, viewModel.getLoyaltyCard().barcodeId, viewModel.getLoyaltyCard().barcodeType, viewModel.getLoyaltyCard().barcodeEncoding, viewModel.getLoyaltyCard().headerColor, 0, null, 0));
viewModel.setLoyaltyCardId((int) DBHelper.insertLoyaltyCard(mDatabase, viewModel.getLoyaltyCard().store, viewModel.getLoyaltyCard().note, viewModel.getLoyaltyCard().validFrom, viewModel.getLoyaltyCard().expiry, viewModel.getLoyaltyCard().balance, viewModel.getLoyaltyCard().balanceType, viewModel.getLoyaltyCard().cardId, viewModel.getLoyaltyCard().barcodeId, viewModel.getLoyaltyCard().barcodeType, viewModel.getLoyaltyCard().headerColor, 0, null, 0));
}
try {
@@ -1634,7 +1597,6 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
String cardIdString = viewModel.getLoyaltyCard().barcodeId != null ? viewModel.getLoyaltyCard().barcodeId : viewModel.getLoyaltyCard().cardId;
CatimaBarcode barcodeFormat = viewModel.getLoyaltyCard().barcodeType;
Charset barcodeEncoding = viewModel.getLoyaltyCard().barcodeEncoding;
if (cardIdString == null || cardIdString.isEmpty() || barcodeFormat == null) {
barcodeImageLayout.setVisibility(View.GONE);
@@ -1654,13 +1616,13 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
barcodeImage.getViewTreeObserver().removeOnGlobalLayoutListener(this);
Log.d(TAG, "ImageView size now known");
BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getApplicationContext(), barcodeImage, cardIdString, barcodeFormat, barcodeEncoding, null, false, LoyaltyCardEditActivity.this, true, false);
BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getApplicationContext(), barcodeImage, cardIdString, barcodeFormat, null, false, LoyaltyCardEditActivity.this, true, false);
viewModel.getTaskHandler().executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter);
}
});
} else {
Log.d(TAG, "ImageView size known known, creating barcode");
BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getApplicationContext(), barcodeImage, cardIdString, barcodeFormat, barcodeEncoding, null, false, this, true, false);
BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getApplicationContext(), barcodeImage, cardIdString, barcodeFormat, null, false, this, true, false);
viewModel.getTaskHandler().executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter);
}
}

View File

@@ -4,12 +4,6 @@ import android.app.Application;
import androidx.appcompat.app.AppCompatDelegate;
import org.acra.ACRA;
import org.acra.config.CoreConfigurationBuilder;
import org.acra.config.DialogConfigurationBuilder;
import org.acra.config.MailSenderConfigurationBuilder;
import org.acra.data.StringFormat;
import protect.card_locker.preferences.Settings;
public class LoyaltyCardLockerApplication extends Application {
@@ -18,27 +12,6 @@ public class LoyaltyCardLockerApplication extends Application {
public void onCreate() {
super.onCreate();
// Initialize crash reporter (if enabled)
if (BuildConfig.useAcraCrashReporter) {
ACRA.init(this, new CoreConfigurationBuilder()
//core configuration:
.withBuildConfigClass(BuildConfig.class)
.withReportFormat(StringFormat.KEY_VALUE_LIST)
.withPluginConfigurations(
new DialogConfigurationBuilder()
.withText(String.format(getString(R.string.acra_catima_has_crashed), getString(R.string.app_name)))
.withCommentPrompt(getString(R.string.acra_explain_crash))
.withResTheme(R.style.AppTheme)
.build(),
new MailSenderConfigurationBuilder()
.withMailTo("acra-crash@catima.app")
.withSubject(String.format(getString(R.string.acra_crash_email_subject), getString(R.string.app_name)))
.build()
)
);
}
// Set theme
Settings settings = new Settings(this);
AppCompatDelegate.setDefaultNightMode(settings.getTheme());
}

View File

@@ -1,9 +1,8 @@
package protect.card_locker;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.database.sqlite.SQLiteDatabase;
@@ -39,7 +38,6 @@ import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
@@ -59,7 +57,6 @@ import com.google.zxing.BarcodeFormat;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.ArrayList;
@@ -89,8 +86,6 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
String cardIdString;
String barcodeIdString;
CatimaBarcode format;
@Nullable
Charset barcodeEncoding;
Bitmap frontImageBitmap;
Bitmap backImageBitmap;
@@ -267,6 +262,19 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
settings = new Settings(this);
String cardOrientation = settings.getCardViewOrientation();
if (cardOrientation.equals(getString(R.string.settings_key_follow_sensor_orientation))) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
} else if (cardOrientation.equals(getString(R.string.settings_key_lock_on_opening_orientation))) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED);
} else if (cardOrientation.equals(getString(R.string.settings_key_portrait_orientation))) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
} else if (cardOrientation.equals(getString(R.string.settings_key_landscape_orientation))) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
}
if (savedInstanceState != null) {
mainImageIndex = savedInstanceState.getInt(STATE_IMAGEINDEX);
isFullscreen = savedInstanceState.getBoolean(STATE_FULLSCREEN);
@@ -690,7 +698,6 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
format = loyaltyCard.barcodeType;
cardIdString = loyaltyCard.cardId;
barcodeIdString = loyaltyCard.barcodeId;
barcodeEncoding = loyaltyCard.barcodeEncoding;
binding.mainImageDescription.setText(loyaltyCard.cardId);
@@ -710,22 +717,10 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(LoyaltyCardViewActivity.this);
builder.setTitle(R.string.cardId);
builder.setView(cardIdView);
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss());
builder.setNeutralButton(R.string.copy_value, (dialog, which) -> {
copyCardIdToClipboard();
});
builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> dialogInterface.dismiss());
AlertDialog dialog = builder.create();
dialog.show();
});
binding.mainImageDescription.setOnLongClickListener(view -> {
if (mainImageIndex != 0) {
// Don't copy to clipboard, we're showing something else
return false;
}
copyCardIdToClipboard();
return true;
});
int backgroundHeaderColor = Utils.getHeaderColor(this, loyaltyCard);
@@ -964,7 +959,6 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
barcodeRenderTarget,
barcodeIdString != null ? barcodeIdString : cardIdString,
format,
barcodeEncoding,
null,
false,
this,
@@ -1104,12 +1098,6 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
}
private void setMainImagePreviousNextButtons() {
// Ensure the main image index is valid. After a card update, some images (front/back/barcode)
// may have been removed, so the index should not exceed the number of available images.
if(mainImageIndex > imageTypes.size() - 1){
mainImageIndex = 0;
}
if (imageTypes.size() < 2) {
binding.mainLeftButton.setVisibility(View.INVISIBLE);
binding.mainRightButton.setVisibility(View.INVISIBLE);
@@ -1266,20 +1254,4 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
);
}
}
private void copyCardIdToClipboard() {
// Take the value thats already displayed to the user
String value = loyaltyCard.cardId;
if (value == null || value.isEmpty()) {
Toast.makeText(this, R.string.nothing_to_copy, Toast.LENGTH_SHORT).show();
return;
}
ClipboardManager cm = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText(getString(R.string.cardId), value);
cm.setPrimaryClip(clip);
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show();
}
}

View File

@@ -0,0 +1,880 @@
package protect.card_locker;
import android.app.Activity;
import android.app.SearchManager;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.CursorIndexOutOfBoundsException;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.CheckBox;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.SearchView;
import androidx.core.splashscreen.SplashScreen;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.tabs.TabLayout;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import protect.card_locker.databinding.ContentMainBinding;
import protect.card_locker.databinding.MainActivityBinding;
import protect.card_locker.databinding.SortingOptionBinding;
import protect.card_locker.preferences.Settings;
import protect.card_locker.preferences.SettingsActivity;
public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCardCursorAdapter.CardAdapterListener {
private MainActivityBinding binding;
private ContentMainBinding contentMainBinding;
private static final String TAG = "Catima";
public static final String RESTART_ACTIVITY_INTENT = "restart_activity_intent";
private static final int MEDIUM_SCALE_FACTOR_DIP = 460;
static final String STATE_SEARCH_QUERY = "SEARCH_QUERY";
private SQLiteDatabase mDatabase;
private LoyaltyCardCursorAdapter mAdapter;
private ActionMode mCurrentActionMode;
private SearchView mSearchView;
private int mLoyaltyCardCount = 0;
protected String mFilter = "";
private String currentQuery = "";
private String finalQuery = "";
protected Object mGroup = null;
protected DBHelper.LoyaltyCardOrder mOrder = DBHelper.LoyaltyCardOrder.Alpha;
protected DBHelper.LoyaltyCardOrderDirection mOrderDirection = DBHelper.LoyaltyCardOrderDirection.Ascending;
protected int selectedTab = 0;
private RecyclerView mCardList;
private View mHelpSection;
private View mNoMatchingCardsText;
private View mNoGroupCardsText;
private TabLayout groupsTabLayout;
private Runnable mUpdateLoyaltyCardListRunnable;
private ActivityResultLauncher<Intent> mBarcodeScannerLauncher;
private ActivityResultLauncher<Intent> mSettingsLauncher;
private ActionMode.Callback mCurrentActionModeCallback = new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode inputMode, Menu inputMenu) {
inputMode.getMenuInflater().inflate(R.menu.card_longclick_menu, inputMenu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode inputMode, Menu inputMenu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode inputMode, MenuItem inputItem) {
if (inputItem.getItemId() == R.id.action_share) {
final ImportURIHelper importURIHelper = new ImportURIHelper(MainActivity.this);
try {
importURIHelper.startShareIntent(mAdapter.getSelectedItems());
} catch (UnsupportedEncodingException e) {
Toast.makeText(MainActivity.this, R.string.failedGeneratingShareURL, Toast.LENGTH_LONG).show();
e.printStackTrace();
}
inputMode.finish();
return true;
} else if (inputItem.getItemId() == R.id.action_edit) {
if (mAdapter.getSelectedItemCount() != 1) {
throw new IllegalArgumentException("Cannot edit more than 1 card at a time");
}
Intent intent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class);
Bundle bundle = new Bundle();
bundle.putInt(LoyaltyCardEditActivity.BUNDLE_ID, mAdapter.getSelectedItems().get(0).id);
bundle.putBoolean(LoyaltyCardEditActivity.BUNDLE_UPDATE, true);
intent.putExtras(bundle);
startActivity(intent);
inputMode.finish();
return true;
} else if (inputItem.getItemId() == R.id.action_delete) {
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(MainActivity.this);
// The following may seem weird, but it is necessary to give translators enough flexibility.
// For example, in Russian, Android's plural quantity "one" actually refers to "any number ending on 1 but not ending in 11".
// So while in English the extra non-plural form seems unnecessary duplication, it is necessary to give translators enough flexibility.
// In here, we use the plain string when meaning exactly 1, and otherwise use the plural forms
if (mAdapter.getSelectedItemCount() == 1) {
builder.setTitle(R.string.deleteTitle);
builder.setMessage(R.string.deleteConfirmation);
} else {
builder.setTitle(getResources().getQuantityString(R.plurals.deleteCardsTitle, mAdapter.getSelectedItemCount(), mAdapter.getSelectedItemCount()));
builder.setMessage(getResources().getQuantityString(R.plurals.deleteCardsConfirmation, mAdapter.getSelectedItemCount(), mAdapter.getSelectedItemCount()));
}
builder.setPositiveButton(R.string.confirm, (dialog, which) -> {
for (LoyaltyCard loyaltyCard : mAdapter.getSelectedItems()) {
Log.d(TAG, "Deleting card: " + loyaltyCard.id);
DBHelper.deleteLoyaltyCard(mDatabase, MainActivity.this, loyaltyCard.id);
ShortcutHelper.removeShortcut(MainActivity.this, loyaltyCard.id);
}
TabLayout.Tab tab = groupsTabLayout.getTabAt(selectedTab);
mGroup = tab != null ? tab.getTag() : null;
updateLoyaltyCardList(true);
dialog.dismiss();
});
builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
AlertDialog dialog = builder.create();
dialog.show();
return true;
} else if (inputItem.getItemId() == R.id.action_archive) {
for (LoyaltyCard loyaltyCard : mAdapter.getSelectedItems()) {
Log.d(TAG, "Archiving card: " + loyaltyCard.id);
DBHelper.updateLoyaltyCardArchiveStatus(mDatabase, loyaltyCard.id, 1);
ShortcutHelper.removeShortcut(MainActivity.this, loyaltyCard.id);
updateLoyaltyCardList(false);
inputMode.finish();
invalidateOptionsMenu();
}
return true;
} else if (inputItem.getItemId() == R.id.action_unarchive) {
for (LoyaltyCard loyaltyCard : mAdapter.getSelectedItems()) {
Log.d(TAG, "Unarchiving card: " + loyaltyCard.id);
DBHelper.updateLoyaltyCardArchiveStatus(mDatabase, loyaltyCard.id, 0);
updateLoyaltyCardList(false);
inputMode.finish();
invalidateOptionsMenu();
}
return true;
} else if (inputItem.getItemId() == R.id.action_star) {
for (LoyaltyCard loyaltyCard : mAdapter.getSelectedItems()) {
Log.d(TAG, "Starring card: " + loyaltyCard.id);
DBHelper.updateLoyaltyCardStarStatus(mDatabase, loyaltyCard.id, 1);
updateLoyaltyCardList(false);
inputMode.finish();
}
return true;
} else if (inputItem.getItemId() == R.id.action_unstar) {
for (LoyaltyCard loyaltyCard : mAdapter.getSelectedItems()) {
Log.d(TAG, "Unstarring card: " + loyaltyCard.id);
DBHelper.updateLoyaltyCardStarStatus(mDatabase, loyaltyCard.id, 0);
updateLoyaltyCardList(false);
inputMode.finish();
}
return true;
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode inputMode) {
mAdapter.clearSelections();
mCurrentActionMode = null;
}
};
@Override
protected void onCreate(Bundle inputSavedInstanceState) {
SplashScreen.installSplashScreen(this);
super.onCreate(inputSavedInstanceState);
// Delete old cache files
// These could be temporary images for the cropper, temporary images in LoyaltyCard toBundle/writeParcel/ etc.
new Thread(() -> {
long twentyFourHoursAgo = System.currentTimeMillis() - (1000 * 60 * 60 * 24);
File[] tempFiles = getCacheDir().listFiles();
if (tempFiles == null) {
Log.e(TAG, "getCacheDir().listFiles() somehow returned null, this should never happen... Skipping cache cleanup...");
return;
}
for (File file : tempFiles) {
if (file.lastModified() < twentyFourHoursAgo) {
if (!file.delete()) {
Log.w(TAG, "Failed to delete cache file " + file.getPath());
}
};
}
}).start();
// We should extract the share intent after we called the super.onCreate as it may need to spawn a dialog window and the app needs to be initialized to not crash
extractIntentFields(getIntent());
binding = MainActivityBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
Utils.applyWindowInsets(binding.getRoot());
setSupportActionBar(binding.toolbar);
groupsTabLayout = binding.groups;
contentMainBinding = ContentMainBinding.bind(binding.include.getRoot());
mDatabase = new DBHelper(this).getWritableDatabase();
mUpdateLoyaltyCardListRunnable = () -> {
updateLoyaltyCardList(false);
};
groupsTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
selectedTab = tab.getPosition();
Log.d("onTabSelected", "Tab Position " + tab.getPosition());
mGroup = tab.getTag();
updateLoyaltyCardList(false);
// Store active tab in Shared Preference to restore next app launch
SharedPreferences activeTabPref = getApplicationContext().getSharedPreferences(
getString(R.string.sharedpreference_active_tab),
Context.MODE_PRIVATE);
SharedPreferences.Editor activeTabPrefEditor = activeTabPref.edit();
activeTabPrefEditor.putInt(getString(R.string.sharedpreference_active_tab), tab.getPosition());
activeTabPrefEditor.apply();
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
mHelpSection = contentMainBinding.helpSection;
mNoMatchingCardsText = contentMainBinding.noMatchingCardsText;
mNoGroupCardsText = contentMainBinding.noGroupCardsText;
mCardList = contentMainBinding.list;
mAdapter = new LoyaltyCardCursorAdapter(this, null, this, mUpdateLoyaltyCardListRunnable);
mCardList.setAdapter(mAdapter);
registerForContextMenu(mCardList);
mBarcodeScannerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
// Exit early if the user cancelled the scan (pressed back/home)
if (result.getResultCode() != RESULT_OK) {
return;
}
Intent editIntent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class);
editIntent.putExtras(result.getData().getExtras());
startActivity(editIntent);
});
mSettingsLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
Intent intent = result.getData();
if (intent != null && intent.getBooleanExtra(RESTART_ACTIVITY_INTENT, false)) {
recreate();
}
}
});
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
if (mSearchView != null && !mSearchView.isIconified()) {
mSearchView.setIconified(true);
} else {
finish();
}
}
});
}
@Override
protected void onResume() {
super.onResume();
if (mCurrentActionMode != null) {
mAdapter.clearSelections();
mCurrentActionMode.finish();
}
if (mSearchView != null && !mSearchView.isIconified()) {
mFilter = mSearchView.getQuery().toString();
}
// Start of active tab logic
updateTabGroups(groupsTabLayout);
// Restore selected tab from Shared Preference
SharedPreferences activeTabPref = getApplicationContext().getSharedPreferences(
getString(R.string.sharedpreference_active_tab),
Context.MODE_PRIVATE);
selectedTab = activeTabPref.getInt(getString(R.string.sharedpreference_active_tab), 0);
// Restore sort preferences from Shared Preferences
mOrder = Utils.getLoyaltyCardOrder(this);
mOrderDirection = Utils.getLoyaltyCardOrderDirection(this);
mGroup = null;
if (groupsTabLayout.getTabCount() != 0) {
TabLayout.Tab tab = groupsTabLayout.getTabAt(selectedTab);
if (tab == null) {
tab = groupsTabLayout.getTabAt(0);
}
groupsTabLayout.selectTab(tab);
assert tab != null;
mGroup = tab.getTag();
} else {
scaleScreen();
}
updateLoyaltyCardList(true);
// End of active tab logic
FloatingActionButton addButton = binding.fabAdd;
addButton.setOnClickListener(v -> {
Intent intent = new Intent(getApplicationContext(), ScanActivity.class);
Bundle bundle = new Bundle();
if (selectedTab != 0) {
bundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, groupsTabLayout.getTabAt(selectedTab).getText().toString());
}
intent.putExtras(bundle);
mBarcodeScannerLauncher.launch(intent);
});
addButton.bringToFront();
var layoutManager = (GridLayoutManager) mCardList.getLayoutManager();
if (layoutManager != null) {
var settings = new Settings(this);
layoutManager.setSpanCount(settings.getPreferredColumnCount());
}
}
private void displayCardSetupOptions(Menu menu, boolean shouldShow) {
for (int id : new int[]{R.id.action_search, R.id.action_display_options, R.id.action_sort}) {
menu.findItem(id).setVisible(shouldShow);
}
}
private void updateLoyaltyCardCount() {
mLoyaltyCardCount = DBHelper.getLoyaltyCardCount(mDatabase);
}
private void updateLoyaltyCardList(boolean updateCount) {
Group group = null;
if (mGroup != null) {
group = (Group) mGroup;
}
mAdapter.swapCursor(DBHelper.getLoyaltyCardCursor(mDatabase, mFilter, group, mOrder, mOrderDirection, mAdapter.showingArchivedCards() ? DBHelper.LoyaltyCardArchiveFilter.All : DBHelper.LoyaltyCardArchiveFilter.Unarchived));
if (updateCount) {
updateLoyaltyCardCount();
// Update menu icons if necessary
invalidateOptionsMenu();
}
if (mLoyaltyCardCount > 0) {
// We want the cardList to be visible regardless of the filtered match count
// to ensure that the noMatchingCardsText doesn't end up being shown below
// the keyboard
mHelpSection.setVisibility(View.GONE);
mNoGroupCardsText.setVisibility(View.GONE);
if (mAdapter.getItemCount() > 0) {
mCardList.setVisibility(View.VISIBLE);
mNoMatchingCardsText.setVisibility(View.GONE);
} else {
mCardList.setVisibility(View.GONE);
if (!mFilter.isEmpty()) {
// Actual Empty Search Result
mNoMatchingCardsText.setVisibility(View.VISIBLE);
mNoGroupCardsText.setVisibility(View.GONE);
} else {
// Group Tab with no Group Cards
mNoMatchingCardsText.setVisibility(View.GONE);
mNoGroupCardsText.setVisibility(View.VISIBLE);
}
}
} else {
mCardList.setVisibility(View.GONE);
mHelpSection.setVisibility(View.VISIBLE);
mNoMatchingCardsText.setVisibility(View.GONE);
mNoGroupCardsText.setVisibility(View.GONE);
}
if (mCurrentActionMode != null) {
mCurrentActionMode.finish();
}
new ListWidget().updateAll(mAdapter.mContext);
}
private void processParseResultList(List<ParseResult> parseResultList, String group, boolean closeAppOnNoBarcode) {
if (parseResultList.isEmpty()) {
throw new IllegalArgumentException("parseResultList may not be empty");
}
Utils.makeUserChooseParseResultFromList(MainActivity.this, parseResultList, new ParseResultListDisambiguatorCallback() {
@Override
public void onUserChoseParseResult(ParseResult parseResult) {
Intent intent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class);
Bundle bundle = parseResult.toLoyaltyCardBundle(MainActivity.this);
if (group != null) {
bundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, group);
}
intent.putExtras(bundle);
startActivity(intent);
}
@Override
public void onUserDismissedSelector() {
if (closeAppOnNoBarcode) {
finish();
}
}
});
}
private void onSharedIntent(Intent intent) {
String receivedAction = intent.getAction();
String receivedType = intent.getType();
if (receivedAction == null || receivedType == null) {
return;
}
List<ParseResult> parseResultList;
// Check for shared text
if (receivedAction.equals(Intent.ACTION_SEND) && receivedType.equals("text/plain")) {
LoyaltyCard loyaltyCard = new LoyaltyCard();
loyaltyCard.setCardId(intent.getStringExtra(Intent.EXTRA_TEXT));
parseResultList = Collections.singletonList(new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard));
} else {
// Parse whatever file was sent, regardless of opening or sharing
Uri data;
if (receivedAction.equals(Intent.ACTION_VIEW)) {
data = intent.getData();
} else if (receivedAction.equals(Intent.ACTION_SEND)) {
data = intent.getParcelableExtra(Intent.EXTRA_STREAM);
} else {
Log.e(TAG, "Wrong action type to parse intent");
return;
}
if (receivedType.startsWith("image/")) {
parseResultList = Utils.retrieveBarcodesFromImage(this, data);
} else if (receivedType.equals("application/pdf")) {
parseResultList = Utils.retrieveBarcodesFromPdf(this, data);
} else if (Arrays.asList("application/vnd.apple.pkpass", "application/vnd-com.apple.pkpass").contains(receivedType)) {
parseResultList = Utils.retrieveBarcodesFromPkPass(this, data);
} else if (receivedType.equals("application/vnd.espass-espass")) {
// FIXME: espass is not pkpass
// However, several users stated in https://github.com/CatimaLoyalty/Android/issues/2197 that the formats are extremely similar to the point they could rename an .espass file to .pkpass and have it imported
// So it makes sense to "unofficially" treat it as a PKPASS for now, even though not completely correct
parseResultList = Utils.retrieveBarcodesFromPkPass(this, data);
} else {
Log.e(TAG, "Wrong mime-type");
return;
}
}
// Give up if we should parse but there is nothing to parse
if (parseResultList == null || parseResultList.isEmpty()) {
finish();
return;
}
processParseResultList(parseResultList, null, true);
}
private void extractIntentFields(Intent intent) {
onSharedIntent(intent);
}
public void updateTabGroups(TabLayout groupsTabLayout) {
List<Group> newGroups = DBHelper.getGroups(mDatabase);
if (newGroups.size() == 0) {
groupsTabLayout.removeAllTabs();
groupsTabLayout.setVisibility(View.GONE);
return;
}
groupsTabLayout.removeAllTabs();
TabLayout.Tab allTab = groupsTabLayout.newTab();
allTab.setText(R.string.all);
allTab.setTag(null);
groupsTabLayout.addTab(allTab, false);
for (Group group : newGroups) {
TabLayout.Tab tab = groupsTabLayout.newTab();
tab.setText(group._id);
tab.setTag(group);
groupsTabLayout.addTab(tab, false);
}
groupsTabLayout.setVisibility(View.VISIBLE);
}
@Override
// Saving currentQuery to finalQuery for user, this will be used to restore search history, happens when user clicks a card from list
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
finalQuery = currentQuery;
// Putting the query also into outState for later use in onRestoreInstanceState when rotating screen
if (mSearchView != null) {
outState.putString(STATE_SEARCH_QUERY, finalQuery);
}
}
@Override
// Restoring instance state when rotation of screen happens with the goal to restore search query for user
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
finalQuery = savedInstanceState.getString(STATE_SEARCH_QUERY, "");
}
@Override
public boolean onCreateOptionsMenu(Menu inputMenu) {
getMenuInflater().inflate(R.menu.main_menu, inputMenu);
displayCardSetupOptions(inputMenu, mLoyaltyCardCount > 0);
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
if (searchManager != null) {
MenuItem searchMenuItem = inputMenu.findItem(R.id.action_search);
mSearchView = (SearchView) searchMenuItem.getActionView();
mSearchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
mSearchView.setSubmitButtonEnabled(false);
mSearchView.setOnCloseListener(() -> {
invalidateOptionsMenu();
return false;
});
/*
* On Android 13 and later, pressing Back while the search view is open hides the keyboard
* and collapses the search view at the same time.
* This brings back the old behavior on Android 12 and lower: pressing Back once
* hides the keyboard, press again while keyboard is hidden to collapse the search view.
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
searchMenuItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
@Override
public boolean onMenuItemActionExpand(@NonNull MenuItem item) {
return true;
}
@Override
public boolean onMenuItemActionCollapse(@NonNull MenuItem item) {
if (mSearchView.hasFocus()) {
mSearchView.clearFocus();
return false;
}
currentQuery = "";
mFilter = "";
updateLoyaltyCardList(false);
return true;
}
});
}
mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
mFilter = newText;
// New logic to ensure search history after coming back from picked card - user will see the last search query
if (newText.isEmpty()) {
if(!finalQuery.isEmpty()){
// Setting the query text for user after coming back from picked card from finalQuery
mSearchView.setQuery(finalQuery, false);
}
else if(!currentQuery.isEmpty()){
// Else if is needed in case user deletes search - expected behaviour is to show all cards
currentQuery = "";
mSearchView.setQuery(currentQuery, false);
}
} else {
// Setting search query each time user changes the text in search to temporary variable to be used later in finalQuery String which will be used to restore search history
currentQuery = newText;
}
TabLayout.Tab currentTab = groupsTabLayout.getTabAt(groupsTabLayout.getSelectedTabPosition());
mGroup = currentTab != null ? currentTab.getTag() : null;
updateLoyaltyCardList(false);
return true;
}
});
// Check if we came from a picked card back to search, in that case we want to show the search view with previous search query
if(!finalQuery.isEmpty()){
// Expand the search view to show the query
searchMenuItem.expandActionView();
// Setting the query text to empty String due to behaviour of onQueryTextChange after coming back from picked card - onQueryTextChange is called automatically without users interaction
finalQuery = "";
mSearchView.setQuery(currentQuery, false);
}
}
return super.onCreateOptionsMenu(inputMenu);
}
@Override
public boolean onOptionsItemSelected(MenuItem inputItem) {
int id = inputItem.getItemId();
if (id == android.R.id.home) {
getOnBackPressedDispatcher().onBackPressed();
}
if (id == R.id.action_display_options) {
mAdapter.showDisplayOptionsDialog();
invalidateOptionsMenu();
return true;
}
if (id == R.id.action_sort) {
AtomicInteger currentIndex = new AtomicInteger();
List<DBHelper.LoyaltyCardOrder> loyaltyCardOrders = Arrays.asList(DBHelper.LoyaltyCardOrder.values());
for (int i = 0; i < loyaltyCardOrders.size(); i++) {
if (mOrder == loyaltyCardOrders.get(i)) {
currentIndex.set(i);
break;
}
}
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(MainActivity.this);
builder.setTitle(R.string.sort_by);
SortingOptionBinding sortingOptionBinding = SortingOptionBinding
.inflate(LayoutInflater.from(MainActivity.this), null, false);
final View customLayout = sortingOptionBinding.getRoot();
builder.setView(customLayout);
CheckBox showReversed = sortingOptionBinding.checkBoxReverse;
showReversed.setChecked(mOrderDirection == DBHelper.LoyaltyCardOrderDirection.Descending);
builder.setSingleChoiceItems(R.array.sort_types_array, currentIndex.get(), (dialog, which) -> currentIndex.set(which));
builder.setPositiveButton(R.string.sort, (dialog, which) -> {
setSort(
loyaltyCardOrders.get(currentIndex.get()),
showReversed.isChecked() ? DBHelper.LoyaltyCardOrderDirection.Descending : DBHelper.LoyaltyCardOrderDirection.Ascending
);
new ListWidget().updateAll(this);
dialog.dismiss();
});
builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
AlertDialog dialog = builder.create();
dialog.show();
return true;
}
if (id == R.id.action_manage_groups) {
Intent i = new Intent(getApplicationContext(), ManageGroupsActivity.class);
startActivity(i);
return true;
}
if (id == R.id.action_import_export) {
Intent i = new Intent(getApplicationContext(), ImportExportActivity.class);
startActivity(i);
return true;
}
if (id == R.id.action_settings) {
Intent i = new Intent(getApplicationContext(), SettingsActivity.class);
mSettingsLauncher.launch(i);
return true;
}
if (id == R.id.action_about) {
Intent i = new Intent(getApplicationContext(), AboutActivity.class);
startActivity(i);
return true;
}
return super.onOptionsItemSelected(inputItem);
}
private void setSort(DBHelper.LoyaltyCardOrder order, DBHelper.LoyaltyCardOrderDirection direction) {
// Update values
mOrder = order;
mOrderDirection = direction;
// Store in Shared Preference to restore next app launch
SharedPreferences sortPref = getApplicationContext().getSharedPreferences(
getString(R.string.sharedpreference_sort),
Context.MODE_PRIVATE);
SharedPreferences.Editor sortPrefEditor = sortPref.edit();
sortPrefEditor.putString(getString(R.string.sharedpreference_sort_order), order.name());
sortPrefEditor.putString(getString(R.string.sharedpreference_sort_direction), direction.name());
sortPrefEditor.apply();
// Update card list
updateLoyaltyCardList(false);
}
@Override
public void onRowLongClicked(int inputPosition) {
enableActionMode(inputPosition);
}
private void enableActionMode(int inputPosition) {
if (mCurrentActionMode == null) {
mCurrentActionMode = startSupportActionMode(mCurrentActionModeCallback);
}
toggleSelection(inputPosition);
}
private void scaleScreen() {
DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int screenHeight = displayMetrics.heightPixels;
float mediumSizePx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,MEDIUM_SCALE_FACTOR_DIP,getResources().getDisplayMetrics());
boolean shouldScaleSmaller = screenHeight < mediumSizePx;
binding.include.welcomeIcon.setVisibility(shouldScaleSmaller ? View.GONE : View.VISIBLE);
}
private void toggleSelection(int inputPosition) {
mAdapter.toggleSelection(inputPosition);
int count = mAdapter.getSelectedItemCount();
if (count == 0) {
mCurrentActionMode.finish();
} else {
mCurrentActionMode.setTitle(getResources().getQuantityString(R.plurals.selectedCardCount, count, count));
MenuItem editItem = mCurrentActionMode.getMenu().findItem(R.id.action_edit);
MenuItem archiveItem = mCurrentActionMode.getMenu().findItem(R.id.action_archive);
MenuItem unarchiveItem = mCurrentActionMode.getMenu().findItem(R.id.action_unarchive);
MenuItem starItem = mCurrentActionMode.getMenu().findItem(R.id.action_star);
MenuItem unstarItem = mCurrentActionMode.getMenu().findItem(R.id.action_unstar);
boolean hasStarred = false;
boolean hasUnstarred = false;
boolean hasArchived = false;
boolean hasUnarchived = false;
for (LoyaltyCard loyaltyCard : mAdapter.getSelectedItems()) {
if (loyaltyCard.starStatus == 1) {
hasStarred = true;
} else {
hasUnstarred = true;
}
if (loyaltyCard.archiveStatus == 1) {
hasArchived = true;
} else {
hasUnarchived = true;
}
// We have all types, no need to keep checking
if (hasStarred && hasUnstarred && hasArchived && hasUnarchived) {
break;
}
}
unarchiveItem.setVisible(hasArchived);
archiveItem.setVisible(hasUnarchived);
if (count == 1) {
starItem.setVisible(!hasStarred);
unstarItem.setVisible(!hasUnstarred);
editItem.setVisible(true);
editItem.setEnabled(true);
} else {
starItem.setVisible(hasUnstarred);
unstarItem.setVisible(hasStarred);
editItem.setVisible(false);
editItem.setEnabled(false);
}
mCurrentActionMode.invalidate();
}
}
@Override
public void onRowClicked(int inputPosition) {
if (mAdapter.getSelectedItemCount() > 0) {
enableActionMode(inputPosition);
} else {
// FIXME
//
// There is a really nasty edge case that can happen when someone taps a card but right
// after it swipes (very small window, hard to reproduce). The cursor gets replaced and
// may not have a card at the ID number that is returned from onRowClicked.
//
// The proper fix, obviously, would involve makes sure an onFling can't happen while a
// click is being processed. Sadly, I have not yet found a way to make that possible.
LoyaltyCard loyaltyCard;
try {
loyaltyCard = mAdapter.getCard(inputPosition);
} catch (CursorIndexOutOfBoundsException e) {
Log.w(TAG, "Prevented crash from tap + swipe on ID " + inputPosition + ": " + e);
return;
}
Intent intent = new Intent(this, LoyaltyCardViewActivity.class);
intent.setAction("");
final Bundle b = new Bundle();
b.putInt(LoyaltyCardViewActivity.BUNDLE_ID, loyaltyCard.id);
ArrayList<Integer> cardList = new ArrayList<>();
for (int i = 0; i < mAdapter.getItemCount(); i++) {
cardList.add(mAdapter.getCard(i).id);
}
b.putIntegerArrayList(LoyaltyCardViewActivity.BUNDLE_CARDLIST, cardList);
intent.putExtras(b);
startActivity(intent);
}
}
}

View File

@@ -1,946 +0,0 @@
package protect.card_locker
import android.app.SearchManager
import android.content.DialogInterface
import android.content.Intent
import android.database.CursorIndexOutOfBoundsException
import android.database.sqlite.SQLiteDatabase
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.util.DisplayMetrics
import android.util.Log
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
import protect.card_locker.DBHelper.LoyaltyCardOrder
import protect.card_locker.DBHelper.LoyaltyCardOrderDirection
import protect.card_locker.LoyaltyCardCursorAdapter.CardAdapterListener
import protect.card_locker.databinding.ContentMainBinding
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 java.io.UnsupportedEncodingException
import java.util.concurrent.atomic.AtomicInteger
import androidx.core.content.edit
class MainActivity : CatimaAppCompatActivity(), CardAdapterListener {
private lateinit var binding: MainActivityBinding
private lateinit var contentMainBinding: ContentMainBinding
private lateinit var mDatabase: SQLiteDatabase
private lateinit var mAdapter: LoyaltyCardCursorAdapter
private var mCurrentActionMode: ActionMode? = null
private var mSearchView: SearchView? = null
private var mLoyaltyCardCount = 0
@JvmField
var mFilter: String = ""
private var currentQuery = ""
private var finalQuery = ""
private var mGroup: Any? = null
private var mOrder: LoyaltyCardOrder = LoyaltyCardOrder.Alpha
private var mOrderDirection: LoyaltyCardOrderDirection = LoyaltyCardOrderDirection.Ascending
private var selectedTab: Int = 0
private lateinit var groupsTabLayout: TabLayout
private lateinit var mUpdateLoyaltyCardListRunnable: Runnable
private lateinit var mBarcodeScannerLauncher: ActivityResultLauncher<Intent>
private lateinit var mSettingsLauncher: ActivityResultLauncher<Intent>
private val mCurrentActionModeCallback: ActionMode.Callback = object : ActionMode.Callback {
override fun onCreateActionMode(inputMode: ActionMode, inputMenu: Menu?): Boolean {
inputMode.menuInflater.inflate(R.menu.card_longclick_menu, inputMenu)
return true
}
override fun onPrepareActionMode(inputMode: ActionMode?, inputMenu: Menu?): Boolean {
return false
}
override fun onActionItemClicked(inputMode: ActionMode, inputItem: MenuItem): Boolean {
when (inputItem.itemId) {
R.id.action_share -> {
try {
ImportURIHelper(this@MainActivity).startShareIntent(mAdapter.getSelectedItems())
} catch (e: UnsupportedEncodingException) {
Toast.makeText(
this@MainActivity,
R.string.failedGeneratingShareURL,
Toast.LENGTH_LONG
).show()
e.printStackTrace()
}
inputMode.finish()
return true
}
R.id.action_edit -> {
require(mAdapter.selectedItemCount == 1) { "Cannot edit more than 1 card at a time" }
startActivity(
Intent(applicationContext, LoyaltyCardEditActivity::class.java).apply {
putExtras(Bundle().apply {
putInt(
LoyaltyCardEditActivity.BUNDLE_ID,
mAdapter.getSelectedItems()[0].id
)
putBoolean(LoyaltyCardEditActivity.BUNDLE_UPDATE, true)
})
}
)
inputMode.finish()
return true
}
R.id.action_duplicate -> {
require(mAdapter.selectedItemCount == 1) { "Cannot duplicate more than 1 card at a time" }
startActivity(
Intent(applicationContext, LoyaltyCardEditActivity::class.java).apply {
putExtras(Bundle().apply {
putInt(
LoyaltyCardEditActivity.BUNDLE_ID,
mAdapter.getSelectedItems()[0].id
)
putBoolean(LoyaltyCardEditActivity.BUNDLE_DUPLICATE_ID, true)
})
}
)
inputMode.finish()
return true
}
R.id.action_delete -> {
MaterialAlertDialogBuilder(this@MainActivity).apply {
// The following may seem weird, but it is necessary to give translators enough flexibility.
// For example, in Russian, Android's plural quantity "one" actually refers to "any number ending on 1 but not ending in 11".
// So while in English the extra non-plural form seems unnecessary duplication, it is necessary to give translators enough flexibility.
// In here, we use the plain string when meaning exactly 1, and otherwise use the plural forms
if (mAdapter.selectedItemCount == 1) {
setTitle(R.string.deleteTitle)
setMessage(R.string.deleteConfirmation)
} else {
setTitle(
getResources().getQuantityString(
R.plurals.deleteCardsTitle,
mAdapter.selectedItemCount,
mAdapter.selectedItemCount
)
)
setMessage(
getResources().getQuantityString(
R.plurals.deleteCardsConfirmation,
mAdapter.selectedItemCount,
mAdapter.selectedItemCount
)
)
}
setPositiveButton(
R.string.confirm
) { dialog, _ ->
for (loyaltyCard in mAdapter.getSelectedItems()) {
Log.d(TAG, "Deleting card: " + loyaltyCard.id)
DBHelper.deleteLoyaltyCard(mDatabase, this@MainActivity, loyaltyCard.id)
ShortcutHelper.removeShortcut(this@MainActivity, loyaltyCard.id)
}
val tab = groupsTabLayout.getTabAt(selectedTab)
mGroup = tab?.tag
updateLoyaltyCardList(true)
dialog.dismiss()
}
setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
}.create().show()
return true
}
R.id.action_archive -> {
for (loyaltyCard in mAdapter.getSelectedItems()) {
Log.d(TAG, "Archiving card: " + loyaltyCard.id)
DBHelper.updateLoyaltyCardArchiveStatus(mDatabase, loyaltyCard.id, 1)
ShortcutHelper.removeShortcut(this@MainActivity, loyaltyCard.id)
updateLoyaltyCardList(false)
inputMode.finish()
invalidateOptionsMenu()
}
return true
}
R.id.action_unarchive -> {
for (loyaltyCard in mAdapter.getSelectedItems()) {
Log.d(TAG, "Unarchiving card: " + loyaltyCard.id)
DBHelper.updateLoyaltyCardArchiveStatus(mDatabase, loyaltyCard.id, 0)
updateLoyaltyCardList(false)
inputMode.finish()
invalidateOptionsMenu()
}
return true
}
R.id.action_star -> {
for (loyaltyCard in mAdapter.getSelectedItems()) {
Log.d(TAG, "Starring card: " + loyaltyCard.id)
DBHelper.updateLoyaltyCardStarStatus(mDatabase, loyaltyCard.id, 1)
updateLoyaltyCardList(false)
inputMode.finish()
}
return true
}
R.id.action_unstar -> {
for (loyaltyCard in mAdapter.getSelectedItems()) {
Log.d(TAG, "Unstarring card: " + loyaltyCard.id)
DBHelper.updateLoyaltyCardStarStatus(mDatabase, loyaltyCard.id, 0)
updateLoyaltyCardList(false)
inputMode.finish()
}
return true
}
}
return false
}
override fun onDestroyActionMode(inputMode: ActionMode?) {
mAdapter.clearSelections()
mCurrentActionMode = null
}
}
override fun onCreate(inputSavedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(inputSavedInstanceState)
// Delete old cache files
// These could be temporary images for the cropper, temporary images in LoyaltyCard toBundle/writeParcel/ etc.
Thread {
val twentyFourHoursAgo = System.currentTimeMillis() - (1000 * 60 * 60 * 24)
val tempFiles = cacheDir.listFiles()
if (tempFiles == null) {
Log.e(
TAG,
"getCacheDir().listFiles() somehow returned null, this should never happen... Skipping cache cleanup..."
)
return@Thread
}
for (file in tempFiles) {
if (file.lastModified() < twentyFourHoursAgo) {
if (!file.delete()) {
Log.w(TAG, "Failed to delete cache file " + file.path)
}
}
}
}.start()
// We should extract the share intent after we called the super.onCreate as it may need to spawn a dialog window and the app needs to be initialized to not crash
extractIntentFields(intent)
binding = MainActivityBinding.inflate(layoutInflater)
setContentView(binding.getRoot())
Utils.applyWindowInsets(binding.getRoot())
setSupportActionBar(binding.toolbar)
groupsTabLayout = binding.groups
contentMainBinding = ContentMainBinding.bind(binding.include.getRoot())
mDatabase = DBHelper(this).writableDatabase
mUpdateLoyaltyCardListRunnable = Runnable {
updateLoyaltyCardList(false)
}
groupsTabLayout.addOnTabSelectedListener(object : OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
selectedTab = tab.position
Log.d("onTabSelected", "Tab Position " + tab.position)
mGroup = tab.tag
updateLoyaltyCardList(false)
// Store active tab in Shared Preference to restore next app launch
applicationContext.getSharedPreferences(
getString(R.string.sharedpreference_active_tab),
MODE_PRIVATE
).edit {
putInt(
getString(R.string.sharedpreference_active_tab),
tab.position
)
}
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
})
mAdapter = LoyaltyCardCursorAdapter(this, null, this, mUpdateLoyaltyCardListRunnable)
contentMainBinding.list.setAdapter(mAdapter)
registerForContextMenu(contentMainBinding.list)
mBarcodeScannerLauncher = registerForActivityResult(
StartActivityForResult(),
ActivityResultCallback registerForActivityResult@{ result: ActivityResult? ->
// Exit early if the user cancelled the scan (pressed back/home)
if (result == null || result.resultCode != RESULT_OK) {
return@registerForActivityResult
}
startActivity(
Intent(applicationContext, LoyaltyCardEditActivity::class.java).apply {
putExtras(result.data!!.extras!!)
}
)
})
mSettingsLauncher = registerForActivityResult(
StartActivityForResult()
) { result: ActivityResult? ->
if (result?.resultCode == RESULT_OK) {
val intent = result.data
if (intent != null && intent.getBooleanExtra(RESTART_ACTIVITY_INTENT, false)) {
recreate()
}
}
}
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (mSearchView != null && !mSearchView!!.isIconified) {
mSearchView!!.isIconified = true
} else {
finish()
}
}
})
}
override fun onResume() {
super.onResume()
if (mCurrentActionMode != null) {
mAdapter.clearSelections()
mCurrentActionMode!!.finish()
}
if (mSearchView != null && !mSearchView!!.isIconified) {
mFilter = mSearchView!!.query.toString()
}
// Start of active tab logic
updateTabGroups(groupsTabLayout)
// Restore selected tab from Shared Preference
selectedTab = applicationContext.getSharedPreferences(
getString(R.string.sharedpreference_active_tab),
MODE_PRIVATE
).getInt(getString(R.string.sharedpreference_active_tab), 0)
// Restore sort preferences from Shared Preferences
mOrder = Utils.getLoyaltyCardOrder(this)
mOrderDirection = Utils.getLoyaltyCardOrderDirection(this)
mGroup = null
if (groupsTabLayout.tabCount != 0) {
var tab = groupsTabLayout.getTabAt(selectedTab)
if (tab == null) {
tab = groupsTabLayout.getTabAt(0)
}
groupsTabLayout.selectTab(tab)
checkNotNull(tab)
mGroup = tab.tag
} else {
scaleScreen()
}
updateLoyaltyCardList(true)
// End of active tab logic
binding.fabAdd.setOnClickListener {
mBarcodeScannerLauncher.launch(
Intent(applicationContext, ScanActivity::class.java).apply {
putExtras(Bundle().apply {
if (selectedTab != 0) {
putString(
LoyaltyCardEditActivity.BUNDLE_ADDGROUP,
groupsTabLayout.getTabAt(selectedTab)!!.text.toString()
)
}
})
}
)
}
binding.fabAdd.bringToFront()
// Apply column count setting to card list
val layoutManager = contentMainBinding.list.layoutManager as GridLayoutManager?
if (layoutManager != null) {
val settings = Settings(this)
layoutManager.setSpanCount(settings.getPreferredColumnCount())
}
}
private fun displayCardSetupOptions(menu: Menu, shouldShow: Boolean) {
for (id in intArrayOf(R.id.action_search, R.id.action_display_options, R.id.action_sort)) {
menu.findItem(id).isVisible = shouldShow
}
}
private fun updateLoyaltyCardCount() {
mLoyaltyCardCount = DBHelper.getLoyaltyCardCount(mDatabase)
}
private fun updateLoyaltyCardList(updateCount: Boolean) {
var group: Group? = null
if (mGroup != null) {
group = mGroup as Group
}
mAdapter.swapCursor(
DBHelper.getLoyaltyCardCursor(
mDatabase,
mFilter,
group,
mOrder,
mOrderDirection,
if (mAdapter.showingArchivedCards()) DBHelper.LoyaltyCardArchiveFilter.All else DBHelper.LoyaltyCardArchiveFilter.Unarchived
)
)
if (updateCount) {
updateLoyaltyCardCount()
// Update menu icons if necessary
invalidateOptionsMenu()
}
if (mLoyaltyCardCount > 0) {
// We want the cardList to be visible regardless of the filtered match count
// to ensure that the noMatchingCardsText doesn't end up being shown below
// the keyboard
contentMainBinding.helpSection.visibility = View.GONE
contentMainBinding.noGroupCardsText.visibility = View.GONE
if (mAdapter.itemCount > 0) {
contentMainBinding.list.visibility = View.VISIBLE
contentMainBinding.noMatchingCardsText.visibility = View.GONE
} else {
contentMainBinding.list.visibility = View.GONE
if (!mFilter.isEmpty()) {
// Actual Empty Search Result
contentMainBinding.noMatchingCardsText.visibility = View.VISIBLE
contentMainBinding.noGroupCardsText.visibility = View.GONE
} else {
// Group Tab with no Group Cards
contentMainBinding.noMatchingCardsText.visibility = View.GONE
contentMainBinding.noGroupCardsText.visibility = View.VISIBLE
}
}
} else {
contentMainBinding.list.visibility = View.GONE
contentMainBinding.helpSection.visibility = View.VISIBLE
contentMainBinding.noMatchingCardsText.visibility = View.GONE
contentMainBinding.noGroupCardsText.visibility = View.GONE
}
if (mCurrentActionMode != null) {
mCurrentActionMode!!.finish()
}
ListWidget().updateAll(mAdapter.mContext)
}
private fun processParseResultList(
parseResultList: MutableList<ParseResult?>,
group: String?,
closeAppOnNoBarcode: Boolean
) {
require(!parseResultList.isEmpty()) { "parseResultList may not be empty" }
Utils.makeUserChooseParseResultFromList(
this@MainActivity,
parseResultList,
object : ParseResultListDisambiguatorCallback {
override fun onUserChoseParseResult(parseResult: ParseResult) {
val intent =
Intent(applicationContext, LoyaltyCardEditActivity::class.java)
val bundle = parseResult.toLoyaltyCardBundle(this@MainActivity)
if (group != null) {
bundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, group)
}
intent.putExtras(bundle)
startActivity(intent)
}
override fun onUserDismissedSelector() {
if (closeAppOnNoBarcode) {
finish()
}
}
})
}
private fun onSharedIntent(intent: Intent) {
val receivedAction = intent.action
val receivedType = intent.type
if (receivedAction == null || receivedType == null) {
return
}
val parseResultList: MutableList<ParseResult?>?
// Check for shared text
if (receivedAction == Intent.ACTION_SEND && receivedType == "text/plain") {
val loyaltyCard = LoyaltyCard()
loyaltyCard.setCardId(intent.getStringExtra(Intent.EXTRA_TEXT)!!)
parseResultList = mutableListOf(ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard))
} else {
// Parse whatever file was sent, regardless of opening or sharing
val data: Uri? = when (receivedAction) {
Intent.ACTION_VIEW -> {
intent.data
}
Intent.ACTION_SEND -> {
intent.getParcelableExtra(Intent.EXTRA_STREAM)
}
else -> {
Log.e(TAG, "Wrong action type to parse intent")
return
}
}
if (receivedType.startsWith("image/")) {
parseResultList = Utils.retrieveBarcodesFromImage(this, data)
} else if (receivedType == "application/pdf") {
parseResultList = Utils.retrieveBarcodesFromPdf(this, data)
} else if (mutableListOf<String?>(
"application/vnd.apple.pkpass",
"application/vnd-com.apple.pkpass"
).contains(receivedType)
) {
parseResultList = Utils.retrieveBarcodesFromPkPass(this, data)
} else if (receivedType == "application/vnd.espass-espass") {
// FIXME: espass is not pkpass
// However, several users stated in https://github.com/CatimaLoyalty/Android/issues/2197 that the formats are extremely similar to the point they could rename an .espass file to .pkpass and have it imported
// So it makes sense to "unofficially" treat it as a PKPASS for now, even though not completely correct
parseResultList = Utils.retrieveBarcodesFromPkPass(this, data)
} else if (receivedType == "application/vnd.apple.pkpasses") {
parseResultList = Utils.retrieveBarcodesFromPkPasses(this, data)
} else {
Log.e(TAG, "Wrong mime-type")
return
}
}
// Give up if we should parse but there is nothing to parse
if (parseResultList == null || parseResultList.isEmpty()) {
finish()
return
}
processParseResultList(parseResultList, null, true)
}
private fun extractIntentFields(intent: Intent) {
onSharedIntent(intent)
}
fun updateTabGroups(groupsTabLayout: TabLayout) {
val newGroups = DBHelper.getGroups(mDatabase)
if (newGroups.isEmpty()) {
groupsTabLayout.removeAllTabs()
groupsTabLayout.visibility = View.GONE
return
}
groupsTabLayout.removeAllTabs()
groupsTabLayout.addTab(
groupsTabLayout.newTab().apply {
setText(R.string.all)
tag = null
},
false
)
for (group in newGroups) {
groupsTabLayout.addTab(
groupsTabLayout.newTab().apply {
text = group._id
tag = group
},
false
)
}
groupsTabLayout.visibility = View.VISIBLE
}
// Saving currentQuery to finalQuery for user, this will be used to restore search history, happens when user clicks a card from list
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
finalQuery = currentQuery
// Putting the query also into outState for later use in onRestoreInstanceState when rotating screen
if (mSearchView != null) {
outState.putString(STATE_SEARCH_QUERY, finalQuery)
}
}
// Restoring instance state when rotation of screen happens with the goal to restore search query for user
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
finalQuery = savedInstanceState.getString(STATE_SEARCH_QUERY, "")
}
override fun onCreateOptionsMenu(inputMenu: Menu): Boolean {
menuInflater.inflate(R.menu.main_menu, inputMenu)
displayCardSetupOptions(inputMenu, mLoyaltyCardCount > 0)
val searchManager = getSystemService(SEARCH_SERVICE) as SearchManager?
if (searchManager != null) {
val searchMenuItem = inputMenu.findItem(R.id.action_search)
mSearchView = searchMenuItem.actionView as SearchView?
mSearchView!!.setSearchableInfo(searchManager.getSearchableInfo(componentName))
mSearchView!!.setSubmitButtonEnabled(false)
mSearchView!!.setOnCloseListener {
invalidateOptionsMenu()
false
}
/*
* On Android 13 and later, pressing Back while the search view is open hides the keyboard
* and collapses the search view at the same time.
* This brings back the old behavior on Android 12 and lower: pressing Back once
* hides the keyboard, press again while keyboard is hidden to collapse the search view.
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
searchMenuItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
if (mSearchView!!.hasFocus()) {
mSearchView!!.clearFocus()
return false
}
currentQuery = ""
mFilter = ""
updateLoyaltyCardList(false)
return true
}
})
}
mSearchView!!.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onQueryTextChange(newText: String): Boolean {
mFilter = newText
// New logic to ensure search history after coming back from picked card - user will see the last search query
if (newText.isEmpty()) {
if (!finalQuery.isEmpty()) {
// Setting the query text for user after coming back from picked card from finalQuery
mSearchView!!.setQuery(finalQuery, false)
} else if (!currentQuery.isEmpty()) {
// Else if is needed in case user deletes search - expected behaviour is to show all cards
currentQuery = ""
mSearchView!!.setQuery(currentQuery, false)
}
} else {
// Setting search query each time user changes the text in search to temporary variable to be used later in finalQuery String which will be used to restore search history
currentQuery = newText
}
val currentTab =
groupsTabLayout.getTabAt(groupsTabLayout.selectedTabPosition)
mGroup = currentTab?.tag
updateLoyaltyCardList(false)
return true
}
})
// Check if we came from a picked card back to search, in that case we want to show the search view with previous search query
if (!finalQuery.isEmpty()) {
// Expand the search view to show the query
searchMenuItem.expandActionView()
// Setting the query text to empty String due to behaviour of onQueryTextChange after coming back from picked card - onQueryTextChange is called automatically without users interaction
finalQuery = ""
mSearchView!!.setQuery(currentQuery, false)
}
}
return super.onCreateOptionsMenu(inputMenu)
}
override fun onOptionsItemSelected(inputItem: MenuItem): Boolean {
when (inputItem.itemId) {
android.R.id.home -> {
onBackPressedDispatcher.onBackPressed()
}
R.id.action_display_options -> {
mAdapter.showDisplayOptionsDialog()
invalidateOptionsMenu()
return true
}
R.id.action_sort -> {
val currentIndex = AtomicInteger()
val loyaltyCardOrders = listOf<LoyaltyCardOrder?>(*LoyaltyCardOrder.entries.toTypedArray())
for (i in loyaltyCardOrders.indices) {
if (mOrder == loyaltyCardOrders[i]) {
currentIndex.set(i)
break
}
}
MaterialAlertDialogBuilder(this@MainActivity).apply {
setTitle(R.string.sort_by)
val sortingOptionBinding = SortingOptionBinding.inflate(LayoutInflater.from(this@MainActivity), null, false)
val customLayout: View = sortingOptionBinding.getRoot()
setView(customLayout)
val showReversed = sortingOptionBinding.checkBoxReverse
showReversed.isChecked = mOrderDirection == LoyaltyCardOrderDirection.Descending
setSingleChoiceItems(
R.array.sort_types_array,
currentIndex.get()
) { _: DialogInterface?, which: Int ->
currentIndex.set(which)
}
setPositiveButton(
R.string.sort
) { dialog, _ ->
setSort(
loyaltyCardOrders[currentIndex.get()]!!,
if (showReversed.isChecked) LoyaltyCardOrderDirection.Descending else LoyaltyCardOrderDirection.Ascending
)
ListWidget().updateAll(this@MainActivity)
dialog?.dismiss()
}
setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
}.create().show()
return true
}
R.id.action_manage_groups -> {
startActivity(
Intent(applicationContext, ManageGroupsActivity::class.java)
)
return true
}
R.id.action_import_export -> {
startActivity(
Intent(applicationContext, ImportExportActivity::class.java)
)
return true
}
R.id.action_settings -> {
mSettingsLauncher.launch(
Intent(applicationContext, SettingsActivity::class.java)
)
return true
}
R.id.action_about -> {
startActivity(
Intent(applicationContext, AboutActivity::class.java)
)
return true
}
}
return super.onOptionsItemSelected(inputItem)
}
private fun setSort(order: LoyaltyCardOrder, direction: LoyaltyCardOrderDirection) {
// Update values
mOrder = order
mOrderDirection = direction
// Store in Shared Preference to restore next app launch
applicationContext.getSharedPreferences(
getString(R.string.sharedpreference_sort),
MODE_PRIVATE
).edit {
putString(
getString(R.string.sharedpreference_sort_order),
order.name
)
putString(
getString(R.string.sharedpreference_sort_direction),
direction.name
)
}
// Update card list
updateLoyaltyCardList(false)
}
override fun onRowLongClicked(inputPosition: Int) {
enableActionMode(inputPosition)
}
private fun enableActionMode(inputPosition: Int) {
if (mCurrentActionMode == null) {
mCurrentActionMode = startSupportActionMode(mCurrentActionModeCallback)
}
toggleSelection(inputPosition)
}
private fun scaleScreen() {
val displayMetrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(displayMetrics)
val screenHeight = displayMetrics.heightPixels
val mediumSizePx = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
MEDIUM_SCALE_FACTOR_DIP.toFloat(),
getResources().displayMetrics
)
val shouldScaleSmaller = screenHeight < mediumSizePx
binding.include.welcomeIcon.visibility = if (shouldScaleSmaller) View.GONE else View.VISIBLE
}
private fun toggleSelection(inputPosition: Int) {
mAdapter.toggleSelection(inputPosition)
val count = mAdapter.selectedItemCount
if (count == 0) {
mCurrentActionMode!!.finish()
} else {
mCurrentActionMode!!.title = getResources().getQuantityString(
R.plurals.selectedCardCount,
count,
count
)
val editItem = mCurrentActionMode!!.menu.findItem(R.id.action_edit)
val duplicateItem = mCurrentActionMode!!.menu.findItem(R.id.action_duplicate)
val archiveItem = mCurrentActionMode!!.menu.findItem(R.id.action_archive)
val unarchiveItem = mCurrentActionMode!!.menu.findItem(R.id.action_unarchive)
val starItem = mCurrentActionMode!!.menu.findItem(R.id.action_star)
val unstarItem = mCurrentActionMode!!.menu.findItem(R.id.action_unstar)
var hasStarred = false
var hasUnstarred = false
var hasArchived = false
var hasUnarchived = false
for (loyaltyCard in mAdapter.getSelectedItems()) {
if (loyaltyCard.starStatus == 1) {
hasStarred = true
} else {
hasUnstarred = true
}
if (loyaltyCard.archiveStatus == 1) {
hasArchived = true
} else {
hasUnarchived = true
}
// We have all types, no need to keep checking
if (hasStarred && hasUnstarred && hasArchived && hasUnarchived) {
break
}
}
unarchiveItem.isVisible = hasArchived
archiveItem.isVisible = hasUnarchived
if (count == 1) {
starItem.isVisible = !hasStarred
unstarItem.isVisible = !hasUnstarred
editItem.isVisible = true
editItem.isEnabled = true
duplicateItem.isVisible = true
duplicateItem.isEnabled = true
} else {
starItem.isVisible = hasUnstarred
unstarItem.isVisible = hasStarred
editItem.isVisible = false
editItem.isEnabled = false
duplicateItem.isVisible = false
duplicateItem.isEnabled = false
}
mCurrentActionMode!!.invalidate()
}
}
override fun onRowClicked(inputPosition: Int) {
if (mAdapter.selectedItemCount > 0) {
enableActionMode(inputPosition)
} else {
// FIXME
//
// There is a really nasty edge case that can happen when someone taps a card but right
// after it swipes (very small window, hard to reproduce). The cursor gets replaced and
// may not have a card at the ID number that is returned from onRowClicked.
//
// The proper fix, obviously, would involve makes sure an onFling can't happen while a
// click is being processed. Sadly, I have not yet found a way to make that possible.
val loyaltyCard: LoyaltyCard
try {
loyaltyCard = mAdapter.getCard(inputPosition)
} catch (e: CursorIndexOutOfBoundsException) {
Log.w(TAG, "Prevented crash from tap + swipe on ID $inputPosition: $e")
return
}
startActivity(
Intent(this, LoyaltyCardViewActivity::class.java).apply {
action = ""
putExtras(Bundle().apply {
putInt(LoyaltyCardViewActivity.BUNDLE_ID, loyaltyCard.id)
val cardList = ArrayList<Int?>()
for (i in 0..<mAdapter.itemCount) {
cardList.add(mAdapter.getCard(i).id)
}
putIntegerArrayList(LoyaltyCardViewActivity.BUNDLE_CARDLIST, cardList)
})
}
)
}
}
companion object {
private const val TAG = "Catima"
const val RESTART_ACTIVITY_INTENT: String = "restart_activity_intent"
private const val MEDIUM_SCALE_FACTOR_DIP = 460
const val STATE_SEARCH_QUERY: String = "SEARCH_QUERY"
}
}

View File

@@ -0,0 +1,242 @@
package protect.card_locker;
import android.content.Intent;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import protect.card_locker.databinding.ActivityManageGroupBinding;
public class ManageGroupActivity extends CatimaAppCompatActivity implements ManageGroupCursorAdapter.CardAdapterListener {
private ActivityManageGroupBinding binding;
private SQLiteDatabase mDatabase;
private ManageGroupCursorAdapter mAdapter;
private final String SAVE_INSTANCE_ADAPTER_STATE = "adapterState";
private final String SAVE_INSTANCE_CURRENT_GROUP_NAME = "currentGroupName";
protected Group mGroup = null;
private RecyclerView mCardList;
private TextView noGroupCardsText;
private EditText mGroupNameText;
private boolean mGroupNameNotInUse;
@Override
protected void onCreate(Bundle inputSavedInstanceState) {
super.onCreate(inputSavedInstanceState);
binding = ActivityManageGroupBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
Utils.applyWindowInsetsAndFabOffset(binding.getRoot(), binding.fabSave);
Toolbar toolbar = binding.toolbar;
setSupportActionBar(toolbar);
mDatabase = new DBHelper(this).getWritableDatabase();
noGroupCardsText = binding.include.noGroupCardsText;
mCardList = binding.include.list;
FloatingActionButton saveButton = binding.fabSave;
mGroupNameText = binding.editTextGroupName;
mGroupNameText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
mGroupNameNotInUse = true;
mGroupNameText.setError(null);
String currentGroupName = mGroupNameText.getText().toString().trim();
if (currentGroupName.length() == 0) {
mGroupNameText.setError(getResources().getText(R.string.group_name_is_empty));
return;
}
if (!mGroup._id.equals(currentGroupName)) {
if (DBHelper.getGroup(mDatabase, currentGroupName) != null) {
mGroupNameNotInUse = false;
mGroupNameText.setError(getResources().getText(R.string.group_name_already_in_use));
} else {
mGroupNameNotInUse = true;
}
}
}
});
Intent intent = getIntent();
String groupId = intent.getStringExtra("group");
if (groupId == null) {
throw (new IllegalArgumentException("this activity expects a group loaded into it's intent"));
}
Log.d("groupId", "groupId: " + groupId);
mGroup = DBHelper.getGroup(mDatabase, groupId);
if (mGroup == null) {
throw (new IllegalArgumentException("cannot load group " + groupId + " from database"));
}
mGroupNameText.setText(mGroup._id);
setTitle(getString(R.string.editGroup, mGroup._id));
mAdapter = new ManageGroupCursorAdapter(this, null, this, mGroup, null);
mCardList.setAdapter(mAdapter);
registerForContextMenu(mCardList);
if (inputSavedInstanceState != null) {
mAdapter.importInGroupState(integerArrayToAdapterState(inputSavedInstanceState.getIntegerArrayList(SAVE_INSTANCE_ADAPTER_STATE)));
mGroupNameText.setText(inputSavedInstanceState.getString(SAVE_INSTANCE_CURRENT_GROUP_NAME));
}
enableToolbarBackButton();
saveButton.setOnClickListener(v -> {
String currentGroupName = mGroupNameText.getText().toString().trim();
if (!currentGroupName.equals(mGroup._id)) {
if (currentGroupName.length() == 0) {
Toast.makeText(getApplicationContext(), R.string.group_name_is_empty, Toast.LENGTH_SHORT).show();
return;
}
if (!mGroupNameNotInUse) {
Toast.makeText(getApplicationContext(), R.string.group_name_already_in_use, Toast.LENGTH_SHORT).show();
return;
}
}
mAdapter.commitToDatabase();
if (!currentGroupName.equals(mGroup._id)) {
DBHelper.updateGroup(mDatabase, mGroup._id, currentGroupName);
}
Toast.makeText(getApplicationContext(), R.string.group_updated, Toast.LENGTH_SHORT).show();
finish();
});
// this setText is here because content_main.xml is reused from main activity
noGroupCardsText.setText(getResources().getText(R.string.noGiftCardsGroup));
updateLoyaltyCardList();
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
leaveWithoutSaving();
}
});
}
private ArrayList<Integer> adapterStateToIntegerArray(HashMap<Integer, Boolean> adapterState) {
ArrayList<Integer> ret = new ArrayList<>(adapterState.size() * 2);
for (Map.Entry<Integer, Boolean> entry : adapterState.entrySet()) {
ret.add(entry.getKey());
ret.add(entry.getValue() ? 1 : 0);
}
return ret;
}
private HashMap<Integer, Boolean> integerArrayToAdapterState(ArrayList<Integer> in) {
HashMap<Integer, Boolean> ret = new HashMap<>();
if (in.size() % 2 != 0) {
throw (new RuntimeException("failed restoring adapterState from integer array list"));
}
for (int i = 0; i < in.size(); i += 2) {
ret.put(in.get(i), in.get(i + 1) == 1);
}
return ret;
}
@Override
public boolean onCreateOptionsMenu(Menu inputMenu) {
getMenuInflater().inflate(R.menu.card_details_menu, inputMenu);
return super.onCreateOptionsMenu(inputMenu);
}
@Override
public boolean onOptionsItemSelected(MenuItem inputItem) {
int id = inputItem.getItemId();
if (id == R.id.action_display_options) {
mAdapter.showDisplayOptionsDialog();
invalidateOptionsMenu();
return true;
}
return super.onOptionsItemSelected(inputItem);
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putIntegerArrayList(SAVE_INSTANCE_ADAPTER_STATE, adapterStateToIntegerArray(mAdapter.exportInGroupState()));
outState.putString(SAVE_INSTANCE_CURRENT_GROUP_NAME, mGroupNameText.getText().toString());
}
private void updateLoyaltyCardList() {
mAdapter.swapCursor(DBHelper.getLoyaltyCardCursor(mDatabase));
if (mAdapter.getItemCount() == 0) {
mCardList.setVisibility(View.GONE);
noGroupCardsText.setVisibility(View.VISIBLE);
} else {
mCardList.setVisibility(View.VISIBLE);
noGroupCardsText.setVisibility(View.GONE);
}
}
private void leaveWithoutSaving() {
if (hasChanged()) {
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(ManageGroupActivity.this);
builder.setTitle(R.string.leaveWithoutSaveTitle);
builder.setMessage(R.string.leaveWithoutSaveConfirmation);
builder.setPositiveButton(R.string.confirm, (dialog, which) -> finish());
builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
AlertDialog dialog = builder.create();
dialog.show();
} else {
finish();
}
}
@Override
public boolean onSupportNavigateUp() {
getOnBackPressedDispatcher().onBackPressed();
return true;
}
private boolean hasChanged() {
return mAdapter.hasChanged() || !mGroup._id.equals(mGroupNameText.getText().toString().trim());
}
@Override
public void onRowLongClicked(int inputPosition) {
mAdapter.toggleSelection(inputPosition);
}
@Override
public void onRowClicked(int inputPosition) {
mAdapter.toggleSelection(inputPosition);
}
}

View File

@@ -1,246 +0,0 @@
package protect.card_locker
import android.content.DialogInterface
import android.database.sqlite.SQLiteDatabase
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.core.widget.doAfterTextChanged
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import protect.card_locker.LoyaltyCardCursorAdapter.CardAdapterListener
import protect.card_locker.databinding.ActivityManageGroupBinding
import protect.card_locker.preferences.Settings
class ManageGroupActivity : CatimaAppCompatActivity(), CardAdapterListener {
private lateinit var binding: ActivityManageGroupBinding
private lateinit var mDatabase: SQLiteDatabase
private lateinit var mAdapter: ManageGroupCursorAdapter
private lateinit var mGroup: Group
private lateinit var mCardList: RecyclerView
private lateinit var noGroupCardsText: TextView
private lateinit var mGroupNameText: EditText
private var mGroupNameNotInUse = false
override fun onCreate(inputSavedInstanceState: Bundle?) {
super.onCreate(inputSavedInstanceState)
binding = ActivityManageGroupBinding.inflate(layoutInflater)
setContentView(binding.root)
Utils.applyWindowInsetsAndFabOffset(binding.root, binding.fabSave)
setSupportActionBar(binding.toolbar)
mDatabase = DBHelper(this).writableDatabase
noGroupCardsText = binding.include.noGroupCardsText
mCardList = binding.include.list
mGroupNameText = binding.editTextGroupName
mGroupNameText.doAfterTextChanged {
mGroupNameNotInUse = true
mGroupNameText.error = null
val currentGroupName = mGroupNameText.text.trim().toString()
if (currentGroupName.isEmpty()) {
mGroupNameText.error = getText(R.string.group_name_is_empty)
return@doAfterTextChanged
}
if (mGroup._id != currentGroupName) {
if (DBHelper.getGroup(mDatabase, currentGroupName) != null) {
mGroupNameNotInUse = false
mGroupNameText.error = getText(R.string.group_name_already_in_use)
} else {
mGroupNameNotInUse = true
}
}
}
val groupId = intent.getStringExtra("group")
?: throw (IllegalArgumentException("this activity expects a group loaded into it's intent"))
Log.d("groupId", "groupId: $groupId")
mGroup = DBHelper.getGroup(mDatabase, groupId)
?: throw IllegalArgumentException("Cannot load group $groupId from database")
mGroupNameText.setText(mGroup._id)
setTitle(getString(R.string.editGroup, mGroup._id))
mAdapter = ManageGroupCursorAdapter(this, null, this, mGroup, null)
mCardList.adapter = mAdapter
registerForContextMenu(mCardList)
if (inputSavedInstanceState != null) {
mAdapter.importInGroupState(
bundleToAdapterState(
adapterStateBundle = inputSavedInstanceState.getBundle(
SAVE_INSTANCE_ADAPTER_STATE
)
)
)
mGroupNameText.setText(
inputSavedInstanceState.getString(
SAVE_INSTANCE_CURRENT_GROUP_NAME
)
)
}
enableToolbarBackButton()
binding.fabSave.setOnClickListener { v: View ->
val currentGroupName = mGroupNameText.text.trim().toString()
if (currentGroupName != mGroup._id) {
when {
currentGroupName.isEmpty() -> {
Toast.makeText(
applicationContext,
R.string.group_name_is_empty,
Toast.LENGTH_SHORT
).show()
return@setOnClickListener
}
!mGroupNameNotInUse -> {
Toast.makeText(
applicationContext,
R.string.group_name_already_in_use,
Toast.LENGTH_SHORT
).show()
return@setOnClickListener
}
}
}
mAdapter.commitToDatabase()
if (currentGroupName != mGroup._id) {
DBHelper.updateGroup(mDatabase, mGroup._id, currentGroupName)
}
Toast.makeText(
applicationContext,
R.string.group_updated,
Toast.LENGTH_SHORT
).show()
finish()
}
// this setText is here because content_main.xml is reused from main activity
noGroupCardsText.text = getText(R.string.noGiftCardsGroup)
updateLoyaltyCardList()
onBackPressedDispatcher.addCallback(
owner = this,
onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
leaveWithoutSaving()
}
}
)
// Apply column count setting to card list
val layoutManager = mCardList.layoutManager as GridLayoutManager?
if (layoutManager != null) {
val settings = Settings(this)
layoutManager.setSpanCount(settings.getPreferredColumnCount())
}
}
private fun adapterStateToBundle(adapterState: HashMap<Int, Boolean>): Bundle {
val adapterStateBundle = Bundle().apply {
for (entry in adapterState.entries) {
putBoolean(entry.key.toString(), entry.value)
}
}
return adapterStateBundle
}
private fun bundleToAdapterState(adapterStateBundle: Bundle?): Map<Int, Boolean> {
adapterStateBundle ?: return emptyMap()
val adapterStateMap = buildMap {
for (key in adapterStateBundle.keySet()) {
put(key.toInt(), adapterStateBundle.getBoolean(key))
}
}
return adapterStateMap
}
override fun onCreateOptionsMenu(inputMenu: Menu): Boolean {
menuInflater.inflate(R.menu.card_details_menu, inputMenu)
return super.onCreateOptionsMenu(inputMenu)
}
override fun onOptionsItemSelected(inputItem: MenuItem): Boolean {
val id = inputItem.itemId
if (id == R.id.action_display_options) {
mAdapter.showDisplayOptionsDialog()
invalidateOptionsMenu()
return true
}
return super.onOptionsItemSelected(inputItem)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBundle(
SAVE_INSTANCE_ADAPTER_STATE,
adapterStateToBundle(mAdapter.exportInGroupState())
)
outState.putString(SAVE_INSTANCE_CURRENT_GROUP_NAME, mGroupNameText.text.toString())
}
private fun updateLoyaltyCardList() {
mAdapter.swapCursor(DBHelper.getLoyaltyCardCursor(mDatabase))
if (mAdapter.itemCount == 0) {
mCardList.visibility = View.GONE
noGroupCardsText.visibility = View.VISIBLE
} else {
mCardList.visibility = View.VISIBLE
noGroupCardsText.visibility = View.GONE
}
}
private fun leaveWithoutSaving() {
if (hasChanged()) {
MaterialAlertDialogBuilder(this@ManageGroupActivity).apply {
setTitle(R.string.leaveWithoutSaveTitle)
setMessage(R.string.leaveWithoutSaveConfirmation)
setPositiveButton(R.string.confirm) { dialog: DialogInterface, _ ->
finish()
}
setNegativeButton(R.string.cancel) { dialog: DialogInterface, _ ->
dialog.dismiss()
}
}.create().show()
} else {
finish()
}
}
override fun onSupportNavigateUp(): Boolean {
onBackPressedDispatcher.onBackPressed()
return true
}
private fun hasChanged(): Boolean {
return mAdapter.hasChanged() || mGroup._id != mGroupNameText.text.trim().toString()
}
override fun onRowLongClicked(inputPosition: Int) {
mAdapter.toggleSelection(inputPosition)
}
override fun onRowClicked(inputPosition: Int) {
mAdapter.toggleSelection(inputPosition)
}
private companion object {
const val SAVE_INSTANCE_ADAPTER_STATE = "adapterState"
const val SAVE_INSTANCE_CURRENT_GROUP_NAME = "currentGroupName"
}
}

View File

@@ -99,7 +99,7 @@ public class ManageGroupCursorAdapter extends LoyaltyCardCursorAdapter {
}
}
public void importInGroupState(Map<Integer, Boolean> cardIdInGroupMap) {
public void importInGroupState(HashMap<Integer, Boolean> cardIdInGroupMap) {
mInGroupOverlay = new HashMap<>(cardIdInGroupMap);
}

View File

@@ -0,0 +1,247 @@
package protect.card_locker;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.text.InputType;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.util.List;
import protect.card_locker.databinding.ManageGroupsActivityBinding;
public class ManageGroupsActivity extends CatimaAppCompatActivity implements GroupCursorAdapter.GroupAdapterListener {
private ManageGroupsActivityBinding binding;
private static final String TAG = "Catima";
private SQLiteDatabase mDatabase;
private TextView mHelpText;
private RecyclerView mGroupList;
GroupCursorAdapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ManageGroupsActivityBinding.inflate(getLayoutInflater());
setTitle(R.string.groups);
setContentView(binding.getRoot());
Utils.applyWindowInsets(binding.getRoot());
Toolbar toolbar = binding.toolbar;
setSupportActionBar(toolbar);
enableToolbarBackButton();
mDatabase = new DBHelper(this).getWritableDatabase();
}
@Override
protected void onResume() {
super.onResume();
FloatingActionButton addButton = binding.fabAdd;
addButton.setOnClickListener(v -> createGroup());
addButton.bringToFront();
mGroupList = binding.include.list;
mHelpText = binding.include.helpText;
// Init group list
RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext());
mGroupList.setLayoutManager(mLayoutManager);
mGroupList.setItemAnimator(new DefaultItemAnimator());
mAdapter = new GroupCursorAdapter(this, null, this);
mGroupList.setAdapter(mAdapter);
updateGroupList();
}
private void updateGroupList() {
mAdapter.swapCursor(DBHelper.getGroupCursor(mDatabase));
if (DBHelper.getGroupCount(mDatabase) == 0) {
mGroupList.setVisibility(View.GONE);
mHelpText.setVisibility(View.VISIBLE);
return;
}
mGroupList.setVisibility(View.VISIBLE);
mHelpText.setVisibility(View.GONE);
}
private void invalidateHomescreenActiveTab() {
SharedPreferences activeTabPref = getApplicationContext().getSharedPreferences(
getString(R.string.sharedpreference_active_tab),
Context.MODE_PRIVATE);
SharedPreferences.Editor activeTabPrefEditor = activeTabPref.edit();
activeTabPrefEditor.putInt(getString(R.string.sharedpreference_active_tab), 0);
activeTabPrefEditor.apply();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
finish();
}
return super.onOptionsItemSelected(item);
}
private void createGroup() {
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
// Header
builder.setTitle(R.string.enter_group_name);
// Layout
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
);
int contentPadding = getResources().getDimensionPixelSize(R.dimen.alert_dialog_content_padding);
params.leftMargin = contentPadding;
params.topMargin = contentPadding / 2;
params.rightMargin = contentPadding;
// EditText with spacing
final EditText input = new EditText(this);
input.setInputType(InputType.TYPE_CLASS_TEXT);
input.setLayoutParams(params);
layout.addView(input);
// Set layout
builder.setView(layout);
// Buttons
builder.setPositiveButton(getString(R.string.ok), (dialog, which) -> {
DBHelper.insertGroup(mDatabase, input.getText().toString().trim());
updateGroupList();
});
builder.setNegativeButton(getString(R.string.cancel), (dialog, which) -> dialog.cancel());
AlertDialog dialog = builder.create();
// Now that the dialog exists, we can bind something that affects the OK button
input.addTextChangedListener(new SimpleTextWatcher() {
public void onTextChanged(CharSequence s, int start, int before, int count) {
String groupName = s.toString().trim();
if (groupName.length() == 0) {
input.setError(getString(R.string.group_name_is_empty));
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
return;
}
if (DBHelper.getGroup(mDatabase, groupName) != null) {
input.setError(getString(R.string.group_name_already_in_use));
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
return;
}
input.setError(null);
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
}
});
dialog.show();
// Disable button (must be done **after** dialog is shown to prevent crash
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
// Set focus on input field
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
input.requestFocus();
}
private String getGroupName(View view) {
TextView groupNameTextView = view.findViewById(R.id.name);
return (String) groupNameTextView.getText();
}
private void moveGroup(View view, boolean up) {
List<Group> groups = DBHelper.getGroups(mDatabase);
final String groupName = getGroupName(view);
int currentIndex = DBHelper.getGroup(mDatabase, groupName).order;
int newIndex;
// Reinsert group in correct position
if (up) {
newIndex = currentIndex - 1;
} else {
newIndex = currentIndex + 1;
}
// Don't try to move out of bounds
if (newIndex < 0 || newIndex >= groups.size()) {
return;
}
Group group = groups.remove(currentIndex);
groups.add(newIndex, group);
// Update database
DBHelper.reorderGroups(mDatabase, groups);
// Update UI
updateGroupList();
// Ordering may have changed, so invalidate
invalidateHomescreenActiveTab();
}
@Override
public void onMoveDownButtonClicked(View view) {
moveGroup(view, false);
}
@Override
public void onMoveUpButtonClicked(View view) {
moveGroup(view, true);
}
@Override
public void onEditButtonClicked(View view) {
Intent intent = new Intent(this, ManageGroupActivity.class);
intent.putExtra("group", getGroupName(view));
startActivity(intent);
}
@Override
public void onDeleteButtonClicked(View view) {
final String groupName = getGroupName(view);
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(R.string.deleteConfirmationGroup);
builder.setMessage(groupName);
builder.setPositiveButton(getString(R.string.ok), (dialog, which) -> {
DBHelper.deleteGroup(mDatabase, groupName);
updateGroupList();
// Delete may change ordering, so invalidate
invalidateHomescreenActiveTab();
});
builder.setNegativeButton(getString(R.string.cancel), (dialog, which) -> dialog.cancel());
AlertDialog dialog = builder.create();
dialog.show();
}
}

View File

@@ -1,240 +0,0 @@
package protect.card_locker
import android.content.DialogInterface
import android.content.Intent
import android.database.sqlite.SQLiteDatabase
import android.os.Bundle
import android.text.InputType
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.core.content.edit
import androidx.core.widget.doOnTextChanged
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import protect.card_locker.GroupCursorAdapter.GroupAdapterListener
import protect.card_locker.databinding.ManageGroupsActivityBinding
class ManageGroupsActivity : CatimaAppCompatActivity(), GroupAdapterListener {
private lateinit var binding: ManageGroupsActivityBinding
private lateinit var mDatabase: SQLiteDatabase
private lateinit var mHelpText: TextView
private lateinit var mGroupList: RecyclerView
private lateinit var mAdapter: GroupCursorAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ManageGroupsActivityBinding.inflate(layoutInflater)
setTitle(R.string.groups)
setContentView(binding.root)
Utils.applyWindowInsets(binding.root)
setSupportActionBar(binding.toolbar)
enableToolbarBackButton()
mDatabase = DBHelper(this).writableDatabase
}
override fun onResume() {
super.onResume()
with(binding.fabAdd) {
setOnClickListener { v: View ->
createGroup()
}
bringToFront()
}
mGroupList = binding.include.list
mHelpText = binding.include.helpText
// Init group list
LinearLayoutManager(applicationContext).apply {
mGroupList.layoutManager = this
}
mGroupList.setItemAnimator(DefaultItemAnimator())
mAdapter = GroupCursorAdapter(this, null, this)
mGroupList.setAdapter(mAdapter)
updateGroupList()
}
private fun updateGroupList() {
mAdapter.swapCursor(DBHelper.getGroupCursor(mDatabase))
if (DBHelper.getGroupCount(mDatabase) == 0) {
mGroupList.visibility = View.GONE
mHelpText.visibility = View.VISIBLE
return
}
mGroupList.visibility = View.VISIBLE
mHelpText.visibility = View.GONE
}
private fun invalidateHomescreenActiveTab() {
val activeTabPref = getSharedPreferences(
getString(R.string.sharedpreference_active_tab),
MODE_PRIVATE
)
activeTabPref.edit {
putInt(getString(R.string.sharedpreference_active_tab), 0)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
finish()
}
return super.onOptionsItemSelected(item)
}
private fun createGroup() {
val builder: AlertDialog.Builder = MaterialAlertDialogBuilder(this)
// Header
builder.setTitle(R.string.enter_group_name)
// Layout
val layout = LinearLayout(this)
layout.orientation = LinearLayout.VERTICAL
val params = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
val contentPadding =
resources.getDimensionPixelSize(R.dimen.alert_dialog_content_padding)
leftMargin = contentPadding
topMargin = contentPadding / 2
rightMargin = contentPadding
}
// EditText with spacing
val input = EditText(this)
input.setInputType(InputType.TYPE_CLASS_TEXT)
input.setLayoutParams(params)
layout.addView(input)
// Set layout
builder.setView(layout)
// Buttons
builder.setPositiveButton(getString(R.string.ok)) { dialog: DialogInterface, which: Int ->
DBHelper.insertGroup(mDatabase, input.text.trim().toString())
updateGroupList()
}
builder.setNegativeButton(getString(R.string.cancel)) { dialog: DialogInterface, which: Int ->
dialog.cancel()
}
val dialog = builder.create()
// Now that the dialog exists, we can bind something that affects the OK button
input.doOnTextChanged { s: CharSequence?, start: Int, before: Int, count: Int ->
val groupName = s?.trim().toString()
if (groupName.isEmpty()) {
input.error = getString(R.string.group_name_is_empty)
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false)
return@doOnTextChanged
}
if (DBHelper.getGroup(mDatabase, groupName) != null) {
input.error = getString(R.string.group_name_already_in_use)
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false)
return@doOnTextChanged
}
input.error = null
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true)
}
dialog.apply {
show()
// Disable button (must be done **after** dialog is shown to prevent crash
getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false)
// Set focus on input field
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
}
input.requestFocus()
}
private fun getGroupName(view: View): String {
val groupNameTextView = view.findViewById<TextView>(R.id.name)
return groupNameTextView.text.toString()
}
private fun moveGroup(view: View, up: Boolean) {
val groups = DBHelper.getGroups(mDatabase)
val groupName = getGroupName(view)
val currentIndex = DBHelper.getGroup(mDatabase, groupName).order
// Reinsert group in correct position
val newIndex: Int = if (up) {
currentIndex - 1
} else {
currentIndex + 1
}
// Don't try to move out of bounds
if (newIndex < 0 || newIndex >= groups.size) {
return
}
val group = groups.removeAt(currentIndex)
groups.add(newIndex, group)
// Update database
DBHelper.reorderGroups(mDatabase, groups)
// Update UI
updateGroupList()
// Ordering may have changed, so invalidate
invalidateHomescreenActiveTab()
}
override fun onMoveDownButtonClicked(view: View) {
moveGroup(view, false)
}
override fun onMoveUpButtonClicked(view: View) {
moveGroup(view, true)
}
override fun onEditButtonClicked(view: View) {
Intent(this, ManageGroupActivity::class.java).apply {
putExtra("group", getGroupName(view))
startActivity(this)
}
}
override fun onDeleteButtonClicked(view: View) {
val groupName = getGroupName(view)
MaterialAlertDialogBuilder(this).apply {
setTitle(R.string.deleteConfirmationGroup)
setMessage(groupName)
setPositiveButton(getString(R.string.ok)) { dialog: DialogInterface, which: Int ->
DBHelper.deleteGroup(mDatabase, groupName)
updateGroupList()
// Delete may change ordering, so invalidate
invalidateHomescreenActiveTab()
}
setNegativeButton(getString(R.string.cancel)) { dialog: DialogInterface, which: Int ->
dialog.cancel()
}
}.create().show()
}
}

View File

@@ -1,17 +1,18 @@
package protect.card_locker;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
public class OpenWebLinkHandler {
private static final String TAG = "Catima";
public void openBrowser(Activity activity, String url) {
public void openBrowser(AppCompatActivity activity, String url) {
if (url == null) {
return;
}

View File

@@ -14,7 +14,6 @@ import org.json.JSONObject
import java.io.FileNotFoundException
import java.io.IOException
import java.math.BigDecimal
import java.nio.charset.Charset
import java.text.DateFormat
import java.text.ParseException
import java.time.ZonedDateTime
@@ -41,7 +40,6 @@ class PkpassParser(context: Context, uri: Uri?) {
private var cardId: String = context.getString(R.string.noBarcode)
private var barcodeId: String? = null
private var barcodeType: CatimaBarcode? = null
private var barcodeEncoding: Charset? = null
private var headerColor: Int? = null
private val starStatus = 0
private val lastUsed: Long = 0
@@ -136,7 +134,6 @@ class PkpassParser(context: Context, uri: Uri?) {
cardId,
barcodeId,
barcodeType,
barcodeEncoding,
headerColor,
starStatus,
lastUsed,
@@ -345,14 +342,13 @@ class PkpassParser(context: Context, uri: Uri?) {
else -> throw IllegalArgumentException("No valid barcode type")
}
// FIXME: We probably need to do something with the messageEncoding field
try {
cardId = barcodeInfo.getString("altText")
barcodeId = barcodeInfo.getString("message")
barcodeEncoding = Charset.forName(barcodeInfo.getString("messageEncoding"))
} catch (ignored: JSONException) {
cardId = barcodeInfo.getString("message")
barcodeId = null
barcodeEncoding = Charset.forName(barcodeInfo.getString("messageEncoding"))
}
// Don't set barcodeId if it's the same as cardId

View File

@@ -1,73 +0,0 @@
package protect.card_locker
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.core.net.toUri
import net.lingala.zip4j.io.inputstream.ZipInputStream
import net.lingala.zip4j.model.LocalFileHeader
import java.io.FileNotFoundException
import java.io.IOException
class PkpassesParser(context: Context, uri: Uri?) {
private var mContext = context
private val pkPassParsers: ArrayList<PkpassParser> = ArrayList()
init {
mContext = context
Log.i(TAG, "Received Pkpasses file")
if (uri == null) {
Log.e(TAG, "Uri did not contain any data")
throw IOException(context.getString(R.string.errorReadingFile))
}
try {
mContext.contentResolver.openInputStream(uri).use { inputStream ->
ZipInputStream(inputStream).use { zipInputStream ->
var localFileHeader: LocalFileHeader?
while (true) {
// Retrieve the next file
localFileHeader = zipInputStream.nextEntry
// If no next file, exit loop
if (localFileHeader == null) {
break
}
// Ignore directories
if (localFileHeader.isDirectory) continue
// Ignore non-pkpass files
if (!localFileHeader.fileName.endsWith(".pkpass")) continue
// Extract .pkpass (.zip) inside .pkpasses to cache directory
val tempFileName = "pkpassparser_" + System.currentTimeMillis() + "_" + localFileHeader.fileName
val tempFile = Utils.copyToTempFile(mContext, zipInputStream, tempFileName)
// Parse temporary file
pkPassParsers.add(
PkpassParser(mContext, tempFile.toUri())
)
// Delete temporary file
tempFile.delete()
}
}
}
} catch (e: FileNotFoundException) {
throw IOException(mContext.getString(R.string.errorReadingFile))
} catch (e: Exception) {
throw e
}
}
fun getPkpassParsers(): ArrayList<PkpassParser> {
return pkPassParsers
}
companion object {
private const val TAG = "Catima"
}
}

View File

@@ -0,0 +1,542 @@
package protect.card_locker;
import static protect.card_locker.BarcodeSelectorActivity.BARCODE_CONTENTS;
import static protect.card_locker.BarcodeSelectorActivity.BARCODE_FORMAT;
import android.Manifest;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.text.InputType;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ListAdapter;
import android.widget.SimpleAdapter;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.zxing.DecodeHintType;
import com.google.zxing.ResultPoint;
import com.google.zxing.client.android.Intents;
import com.journeyapps.barcodescanner.BarcodeCallback;
import com.journeyapps.barcodescanner.BarcodeResult;
import com.journeyapps.barcodescanner.CaptureManager;
import com.journeyapps.barcodescanner.DecoratedBarcodeView;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import protect.card_locker.databinding.CustomBarcodeScannerBinding;
import protect.card_locker.databinding.ScanActivityBinding;
/**
* Custom Scannner Activity extending from Activity to display a custom layout form scanner view.
* <p>
* Based on https://github.com/journeyapps/zxing-android-embedded/blob/0fdfbce9fb3285e985bad9971c5f7c0a7a334e7b/sample/src/main/java/example/zxing/CustomScannerActivity.java
* originally licensed under Apache 2.0
*/
public class ScanActivity extends CatimaAppCompatActivity {
private ScanActivityBinding binding;
private CustomBarcodeScannerBinding customBarcodeScannerBinding;
private static final String TAG = "Catima";
private static final int MEDIUM_SCALE_FACTOR_DIP = 460;
private static final int COMPAT_SCALE_FACTOR_DIP = 320;
private static final int PERMISSION_SCAN_ADD_FROM_IMAGE = 100;
private static final int PERMISSION_SCAN_ADD_FROM_PDF = 101;
private static final int PERMISSION_SCAN_ADD_FROM_PKPASS = 102;
private CaptureManager capture;
private DecoratedBarcodeView barcodeScannerView;
private String cardId;
private String addGroup;
private boolean torch = false;
private ActivityResultLauncher<Intent> manualAddLauncher;
// can't use the pre-made contract because that launches the file manager for image type instead of gallery
private ActivityResultLauncher<Intent> photoPickerLauncher;
private ActivityResultLauncher<Intent> pdfPickerLauncher;
private ActivityResultLauncher<Intent> pkpassPickerLauncher;
static final String STATE_SCANNER_ACTIVE = "scannerActive";
private boolean mScannerActive = true;
private boolean mHasError = false;
private void extractIntentFields(Intent intent) {
final Bundle b = intent.getExtras();
cardId = b != null ? b.getString(LoyaltyCard.BUNDLE_LOYALTY_CARD_CARD_ID) : null;
addGroup = b != null ? b.getString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP) : null;
Log.d(TAG, "Scan activity: id=" + cardId);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ScanActivityBinding.inflate(getLayoutInflater());
customBarcodeScannerBinding = CustomBarcodeScannerBinding.bind(binding.zxingBarcodeScanner);
setTitle(R.string.scanCardBarcode);
setContentView(binding.getRoot());
Utils.applyWindowInsets(binding.getRoot());
Toolbar toolbar = binding.toolbar;
setSupportActionBar(toolbar);
enableToolbarBackButton();
extractIntentFields(getIntent());
manualAddLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.SELECT_BARCODE_REQUEST, result.getResultCode(), result.getData()));
photoPickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.BARCODE_IMPORT_FROM_IMAGE_FILE, result.getResultCode(), result.getData()));
pdfPickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.BARCODE_IMPORT_FROM_PDF_FILE, result.getResultCode(), result.getData()));
pkpassPickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.BARCODE_IMPORT_FROM_PKPASS_FILE, result.getResultCode(), result.getData()));
customBarcodeScannerBinding.fabOtherOptions.setOnClickListener(view -> {
setScannerActive(false);
ArrayList<HashMap<String, Object>> list = new ArrayList<>();
String[] texts = new String[]{
getString(R.string.addWithoutBarcode),
getString(R.string.addManually),
getString(R.string.addFromImage),
getString(R.string.addFromPdfFile),
getString(R.string.addFromPkpass)
};
Object[] icons = new Object[]{
R.drawable.baseline_block_24,
R.drawable.ic_edit,
R.drawable.baseline_image_24,
R.drawable.baseline_picture_as_pdf_24,
R.drawable.local_activity_24px
};
String[] columns = new String[]{"text", "icon"};
for (int i = 0; i < texts.length; i++) {
HashMap<String, Object> map = new HashMap<>();
map.put(columns[0], texts[i]);
map.put(columns[1], icons[i]);
list.add(map);
}
ListAdapter adapter = new SimpleAdapter(
ScanActivity.this,
list,
R.layout.alertdialog_row_with_icon,
columns,
new int[]{R.id.textView, R.id.imageView}
);
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(ScanActivity.this);
builder.setTitle(getString(R.string.add_a_card_in_a_different_way));
builder.setAdapter(
adapter,
(dialogInterface, i) -> {
switch (i) {
case 0:
addWithoutBarcode();
break;
case 1:
addManually();
break;
case 2:
addFromImage();
break;
case 3:
addFromPdf();
break;
case 4:
addFromPkPass();
break;
default:
throw new IllegalArgumentException("Unknown 'Add a card in a different way' dialog option");
}
}
);
builder.setOnCancelListener(dialogInterface -> setScannerActive(true));
builder.show();
});
// Configure barcodeScanner
barcodeScannerView = binding.zxingBarcodeScanner;
Intent barcodeScannerIntent = new Intent();
Bundle barcodeScannerIntentBundle = new Bundle();
barcodeScannerIntentBundle.putBoolean(DecodeHintType.ALSO_INVERTED.name(), Boolean.TRUE);
barcodeScannerIntent.putExtras(barcodeScannerIntentBundle);
barcodeScannerView.initializeFromIntent(barcodeScannerIntent);
// Even though we do the actual decoding with the barcodeScannerView
// CaptureManager needs to be running to show the camera and scanning bar
capture = new CatimaCaptureManager(this, barcodeScannerView, this::onCaptureManagerError);
Intent captureIntent = new Intent();
Bundle captureIntentBundle = new Bundle();
captureIntentBundle.putBoolean(Intents.Scan.BEEP_ENABLED, false);
captureIntent.putExtras(captureIntentBundle);
capture.initializeFromIntent(captureIntent, savedInstanceState);
barcodeScannerView.decodeSingle(new BarcodeCallback() {
@Override
public void barcodeResult(BarcodeResult result) {
LoyaltyCard loyaltyCard = new LoyaltyCard();
loyaltyCard.setCardId(result.getText());
loyaltyCard.setBarcodeType(CatimaBarcode.fromBarcode(result.getBarcodeFormat()));
returnResult(new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard));
}
@Override
public void possibleResultPoints(List<ResultPoint> resultPoints) {
}
});
}
@Override
protected void onResume() {
super.onResume();
if (mScannerActive) {
capture.onResume();
}
if (!Utils.deviceHasCamera(this)) {
showCameraError(getString(R.string.noCameraFoundGuideText), false);
} else if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
showCameraPermissionMissingText();
} else {
hideCameraError();
}
scaleScreen();
}
@Override
protected void onPause() {
super.onPause();
capture.onPause();
}
@Override
protected void onDestroy() {
super.onDestroy();
capture.onDestroy();
}
@Override
protected void onSaveInstanceState(Bundle savedInstanceState) {
super.onSaveInstanceState(savedInstanceState);
capture.onSaveInstanceState(savedInstanceState);
savedInstanceState.putBoolean(STATE_SCANNER_ACTIVE, mScannerActive);
}
@Override
public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
mScannerActive = savedInstanceState.getBoolean(STATE_SCANNER_ACTIVE);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
return barcodeScannerView.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)) {
getMenuInflater().inflate(R.menu.scan_menu, menu);
}
barcodeScannerView.setTorchOff();
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
setResult(Activity.RESULT_CANCELED);
finish();
return true;
} else if (item.getItemId() == R.id.action_toggle_flashlight) {
if (torch) {
torch = false;
barcodeScannerView.setTorchOff();
item.setTitle(R.string.turn_flashlight_on);
item.setIcon(R.drawable.ic_flashlight_off_white_24dp);
} else {
torch = true;
barcodeScannerView.setTorchOn();
item.setTitle(R.string.turn_flashlight_off);
item.setIcon(R.drawable.ic_flashlight_on_white_24dp);
}
}
return super.onOptionsItemSelected(item);
}
private void setScannerActive(boolean isActive) {
if (isActive) {
barcodeScannerView.resume();
} else {
barcodeScannerView.pause();
}
mScannerActive = isActive;
}
private void returnResult(ParseResult parseResult) {
Intent result = new Intent();
Bundle bundle = parseResult.toLoyaltyCardBundle(ScanActivity.this);
if (addGroup != null) {
bundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, addGroup);
}
result.putExtras(bundle);
ScanActivity.this.setResult(RESULT_OK, result);
finish();
}
private void handleActivityResult(int requestCode, int resultCode, Intent intent) {
super.onActivityResult(requestCode, resultCode, intent);
List<ParseResult> parseResultList = Utils.parseSetBarcodeActivityResult(requestCode, resultCode, intent, this);
if (parseResultList.isEmpty()) {
setScannerActive(true);
return;
}
Utils.makeUserChooseParseResultFromList(this, parseResultList, new ParseResultListDisambiguatorCallback() {
@Override
public void onUserChoseParseResult(ParseResult parseResult) {
returnResult(parseResult);
}
@Override
public void onUserDismissedSelector() {
setScannerActive(true);
}
});
}
private void addWithoutBarcode() {
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
builder.setOnCancelListener(dialogInterface -> setScannerActive(true));
// Header
builder.setTitle(R.string.addWithoutBarcode);
// Layout
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
);
int contentPadding = getResources().getDimensionPixelSize(R.dimen.alert_dialog_content_padding);
params.leftMargin = contentPadding;
params.topMargin = contentPadding / 2;
params.rightMargin = contentPadding;
// Description
TextView currentTextview = new TextView(this);
currentTextview.setText(getString(R.string.enter_card_id));
currentTextview.setLayoutParams(params);
layout.addView(currentTextview);
// EditText with spacing
final EditText input = new EditText(this);
input.setInputType(InputType.TYPE_CLASS_TEXT);
input.setLayoutParams(params);
layout.addView(input);
// Set layout
builder.setView(layout);
// Buttons
builder.setPositiveButton(getString(R.string.ok), (dialog, which) -> {
LoyaltyCard loyaltyCard = new LoyaltyCard();
loyaltyCard.setCardId(input.getText().toString());
returnResult(new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard));
});
builder.setNegativeButton(getString(R.string.cancel), (dialog, which) -> dialog.cancel());
AlertDialog dialog = builder.create();
// Now that the dialog exists, we can bind something that affects the OK button
input.addTextChangedListener(new SimpleTextWatcher() {
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (s.length() == 0) {
input.setError(getString(R.string.card_id_must_not_be_empty));
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
} else {
input.setError(null);
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
}
}
});
dialog.show();
// Disable button (must be done **after** dialog is shown to prevent crash
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
// Set focus on input field
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
input.requestFocus();
}
public void addManually() {
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(ScanActivity.this);
builder.setTitle(R.string.add_manually_warning_title);
builder.setMessage(R.string.add_manually_warning_message);
builder.setPositiveButton(R.string.continue_, (dialog, which) -> {
Intent i = new Intent(getApplicationContext(), BarcodeSelectorActivity.class);
if (cardId != null) {
final Bundle b = new Bundle();
b.putString(LoyaltyCard.BUNDLE_LOYALTY_CARD_CARD_ID, cardId);
i.putExtras(b);
}
manualAddLauncher.launch(i);
});
builder.setNegativeButton(R.string.cancel, (dialog, which) -> setScannerActive(true));
builder.setOnCancelListener(dialog -> setScannerActive(true));
builder.show();
}
public void addFromImage() {
PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_IMAGE);
}
public void addFromPdf() {
PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_PDF);
}
public void addFromPkPass() {
PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_PKPASS);
}
private void addFromImageOrFileAfterPermission(String mimeType, ActivityResultLauncher<Intent> launcher, int chooserText, int errorMessage) {
Intent photoPickerIntent = new Intent(Intent.ACTION_PICK);
photoPickerIntent.setType(mimeType);
Intent contentIntent = new Intent(Intent.ACTION_GET_CONTENT);
contentIntent.setType(mimeType);
Intent chooserIntent = Intent.createChooser(photoPickerIntent, getString(chooserText));
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { contentIntent });
try {
launcher.launch(chooserIntent);
} catch (ActivityNotFoundException e) {
setScannerActive(true);
Toast.makeText(getApplicationContext(), errorMessage, Toast.LENGTH_LONG).show();
Log.e(TAG, "No activity found to handle intent", e);
}
}
public void onCaptureManagerError(String errorMessage) {
if (mHasError) {
// We're already showing an error, ignore this new error
return;
}
showCameraError(errorMessage, false);
}
private void showCameraPermissionMissingText() {
showCameraError(getString(R.string.noCameraPermissionDirectToSystemSetting), true);
}
private void showCameraError(String message, boolean setOnClick) {
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorMessage.setText(message);
setCameraErrorState(true, setOnClick);
}
private void hideCameraError() {
setCameraErrorState(false, false);
}
private void setCameraErrorState(boolean visible, boolean setOnClick) {
mHasError = visible;
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorClickableArea.setOnClickListener(visible && setOnClick ? v -> {
navigateToSystemPermissionSetting();
} : null);
customBarcodeScannerBinding.cardInputContainer.setBackgroundColor(visible ? obtainThemeAttribute(com.google.android.material.R.attr.colorSurface) : Color.TRANSPARENT);
customBarcodeScannerBinding.cameraErrorLayout.getRoot().setVisibility(visible ? View.VISIBLE : View.GONE);
}
private void scaleScreen() {
DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int screenHeight = displayMetrics.heightPixels;
float mediumSizePx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,MEDIUM_SCALE_FACTOR_DIP,getResources().getDisplayMetrics());
boolean shouldScaleSmaller = screenHeight < mediumSizePx;
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorIcon.setVisibility(shouldScaleSmaller ? View.GONE : View.VISIBLE);
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorTitle.setVisibility(shouldScaleSmaller ? View.GONE : View.VISIBLE);
}
private int obtainThemeAttribute(int attribute) {
TypedValue typedValue = new TypedValue();
getTheme().resolveAttribute(attribute, typedValue, true);
return typedValue.data;
}
private void navigateToSystemPermissionSetting() {
Intent permissionIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", getPackageName(), null));
permissionIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(permissionIntent);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
onMockedRequestPermissionsResult(requestCode, permissions, grantResults);
}
public void onMockedRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
if (requestCode == CaptureManager.getCameraPermissionReqCode()) {
if (granted) {
hideCameraError();
} else {
showCameraPermissionMissingText();
}
} else if (requestCode == PERMISSION_SCAN_ADD_FROM_IMAGE || requestCode == PERMISSION_SCAN_ADD_FROM_PDF || requestCode == PERMISSION_SCAN_ADD_FROM_PKPASS) {
if (granted) {
if (requestCode == PERMISSION_SCAN_ADD_FROM_IMAGE) {
addFromImageOrFileAfterPermission("image/*", photoPickerLauncher, R.string.addFromImage, R.string.failedLaunchingPhotoPicker);
} else if (requestCode == PERMISSION_SCAN_ADD_FROM_PDF) {
addFromImageOrFileAfterPermission("application/pdf", pdfPickerLauncher, R.string.addFromPdfFile, R.string.failedLaunchingFileManager);
} else {
addFromImageOrFileAfterPermission("application/*", pkpassPickerLauncher, R.string.addFromPkpass, R.string.failedLaunchingFileManager);
}
} else {
setScannerActive(true);
Toast.makeText(this, R.string.storageReadPermissionRequired, Toast.LENGTH_LONG).show();
}
}
}
}

View File

@@ -1,599 +0,0 @@
package protect.card_locker
import android.Manifest
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.text.InputType
import android.util.DisplayMetrics
import android.util.Log
import android.util.TypedValue
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.ListAdapter
import android.widget.SimpleAdapter
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.widget.doOnTextChanged
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.zxing.DecodeHintType
import com.google.zxing.ResultPoint
import com.journeyapps.barcodescanner.BarcodeCallback
import com.journeyapps.barcodescanner.BarcodeResult
import com.journeyapps.barcodescanner.CaptureManager
import com.journeyapps.barcodescanner.DecoratedBarcodeView
import protect.card_locker.databinding.CustomBarcodeScannerBinding
import protect.card_locker.databinding.ScanActivityBinding
/**
* Custom Scannner Activity extending from Activity to display a custom layout form scanner view.
* <p>
* Based on https://github.com/journeyapps/zxing-android-embedded/blob/0fdfbce9fb3285e985bad9971c5f7c0a7a334e7b/sample/src/main/java/example/zxing/CustomScannerActivity.java
* originally licensed under Apache 2.0
*/
class ScanActivity : CatimaAppCompatActivity() {
private lateinit var binding: ScanActivityBinding
private lateinit var customBarcodeScannerBinding: CustomBarcodeScannerBinding
companion object {
private const val TAG = "Catima"
private const val MEDIUM_SCALE_FACTOR_DIP = 460
private const val COMPAT_SCALE_FACTOR_DIP = 320
private const val PERMISSION_SCAN_ADD_FROM_IMAGE = 100
private const val PERMISSION_SCAN_ADD_FROM_PDF = 101
private const val PERMISSION_SCAN_ADD_FROM_PKPASS = 102
private const val STATE_SCANNER_ACTIVE = "scannerActive"
}
private lateinit var capture: CaptureManager
private lateinit var barcodeScannerView: DecoratedBarcodeView
private var cardId: String? = null
private var addGroup: String? = null
private var torch = false
private lateinit var manualAddLauncher: ActivityResultLauncher<Intent>
// can't use the pre-made contract because that launches the file manager for image type instead of gallery
private lateinit var photoPickerLauncher: ActivityResultLauncher<Intent>
private lateinit var pdfPickerLauncher: ActivityResultLauncher<Intent>
private lateinit var pkpassPickerLauncher: ActivityResultLauncher<Intent>
private var mScannerActive = true
private var mHasError = false
private fun extractIntentFields(intent: Intent) {
val b = intent.extras
cardId = b?.getString(LoyaltyCard.BUNDLE_LOYALTY_CARD_CARD_ID)
addGroup = b?.getString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP)
Log.d(TAG, "Scan activity: id=$cardId")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ScanActivityBinding.inflate(layoutInflater)
customBarcodeScannerBinding = CustomBarcodeScannerBinding.bind(binding.zxingBarcodeScanner)
setTitle(R.string.scanCardBarcode)
setContentView(binding.root)
Utils.applyWindowInsets(binding.root)
setSupportActionBar(binding.toolbar)
enableToolbarBackButton()
extractIntentFields(intent)
manualAddLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
handleActivityResult(
Utils.SELECT_BARCODE_REQUEST,
result.resultCode,
result.data
)
}
photoPickerLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
handleActivityResult(
Utils.BARCODE_IMPORT_FROM_IMAGE_FILE,
result.resultCode,
result.data
)
}
pdfPickerLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
handleActivityResult(
Utils.BARCODE_IMPORT_FROM_PDF_FILE,
result.resultCode,
result.data
)
}
pkpassPickerLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
handleActivityResult(
Utils.BARCODE_IMPORT_FROM_PKPASS_FILE,
result.resultCode,
result.data
)
}
customBarcodeScannerBinding.fabOtherOptions.setOnClickListener {
setScannerActive(false)
val list: ArrayList<HashMap<String, Any>> = arrayListOf()
val texts = arrayOf(
getString(R.string.addWithoutBarcode),
getString(R.string.addManually),
getString(R.string.addFromImage),
getString(R.string.addFromPdfFile),
getString(R.string.addFromPkpass)
)
val icons = arrayOf(
R.drawable.baseline_block_24,
R.drawable.ic_edit,
R.drawable.baseline_image_24,
R.drawable.baseline_picture_as_pdf_24,
R.drawable.local_activity_24px
)
val columns = arrayOf("text", "icon")
for (i in 0 until texts.size) {
val map: HashMap<String, Any> = hashMapOf()
map.put(columns[0], texts[i])
map.put(columns[1], icons[i])
list.add(map)
}
val adapter: ListAdapter = SimpleAdapter(
this,
list,
R.layout.alertdialog_row_with_icon,
columns,
intArrayOf(R.id.textView, R.id.imageView)
)
val builder = MaterialAlertDialogBuilder(this).apply {
setTitle(getString(R.string.add_a_card_in_a_different_way))
setAdapter(adapter) { _, i ->
when (i) {
0 -> addWithoutBarcode()
1 -> addManually()
2 -> addFromImage()
3 -> addFromPdf()
4 -> addFromPkPass()
else -> throw IllegalArgumentException(
"Unknown 'Add a card in a different way' dialog option: $i"
)
}
}
setOnCancelListener { _ -> setScannerActive(true) }
}
builder.show()
}
// Configure barcodeScanner
barcodeScannerView = binding.zxingBarcodeScanner
val barcodeScannerIntent = Intent().apply {
val barcodeScannerIntentBundle = Bundle().apply {
putBoolean(DecodeHintType.ALSO_INVERTED.name, true)
}
putExtras(barcodeScannerIntentBundle)
}
barcodeScannerView.initializeFromIntent(barcodeScannerIntent)
// Even though we do the actual decoding with the barcodeScannerView
// CaptureManager needs to be running to show the camera and scanning bar
capture = CatimaCaptureManager(this, barcodeScannerView, this::onCaptureManagerError)
val captureIntent = Intent().apply {
val captureIntentBundle = Bundle().apply {
putBoolean(DecodeHintType.ALSO_INVERTED.name, false)
}
putExtras(captureIntentBundle)
}
capture.initializeFromIntent(captureIntent, savedInstanceState)
barcodeScannerView.decodeSingle(object : BarcodeCallback {
override fun barcodeResult(result: BarcodeResult) {
val loyaltyCard = LoyaltyCard().apply {
setCardId(result.text)
setBarcodeType(CatimaBarcode.fromBarcode(result.barcodeFormat))
}
returnResult(ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard))
}
override fun possibleResultPoints(resultPoints: List<ResultPoint?>?) {}
})
}
override fun onResume() {
super.onResume()
if (mScannerActive) {
capture.onResume()
}
if (!Utils.deviceHasCamera(this)) {
showCameraError(getString(R.string.noCameraFoundGuideText), false)
} else if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.CAMERA
) != PackageManager.PERMISSION_GRANTED
) {
showCameraPermissionMissingText()
} else {
hideCameraError()
}
scaleScreen()
}
override fun onPause() {
super.onPause()
capture.onPause()
}
override fun onDestroy() {
super.onDestroy()
capture.onDestroy()
}
override fun onSaveInstanceState(savedInstanceState: Bundle) {
super.onSaveInstanceState(savedInstanceState)
capture.onSaveInstanceState(savedInstanceState)
savedInstanceState.putBoolean(STATE_SCANNER_ACTIVE, mScannerActive)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
mScannerActive = savedInstanceState.getBoolean(STATE_SCANNER_ACTIVE)
}
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
return barcodeScannerView.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
if (packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)) {
menuInflater.inflate(R.menu.scan_menu, menu)
}
barcodeScannerView.setTorchOff()
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
setResult(RESULT_CANCELED)
finish()
return true
} else if (item.itemId == R.id.action_toggle_flashlight) {
if (torch) {
torch = false
barcodeScannerView.setTorchOff()
item.setTitle(R.string.turn_flashlight_on)
item.setIcon(R.drawable.ic_flashlight_off_white_24dp)
} else {
torch = true
barcodeScannerView.setTorchOn()
item.setTitle(R.string.turn_flashlight_off)
item.setIcon(R.drawable.ic_flashlight_on_white_24dp)
}
}
return super.onOptionsItemSelected(item)
}
private fun setScannerActive(isActive: Boolean) {
if (isActive) {
barcodeScannerView.resume()
} else {
barcodeScannerView.pause()
}
mScannerActive = isActive
}
private fun returnResult(parseResult: ParseResult) {
val bundle = parseResult.toLoyaltyCardBundle(this).apply {
addGroup?.let { putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, it) }
}
val result = Intent().apply { putExtras(bundle) }
this.setResult(RESULT_OK, result)
finish()
}
private fun handleActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
super.onActivityResult(resultCode, resultCode, intent)
val parseResultList: List<ParseResult> =
Utils.parseSetBarcodeActivityResult(requestCode, resultCode, intent, this)
if (parseResultList.isEmpty()) {
setScannerActive(true)
return
}
Utils.makeUserChooseParseResultFromList(
this,
parseResultList,
object : ParseResultListDisambiguatorCallback {
override fun onUserChoseParseResult(parseResult: ParseResult) {
returnResult(parseResult)
}
override fun onUserDismissedSelector() {
setScannerActive(true)
}
})
}
private fun addWithoutBarcode() {
val builder: AlertDialog.Builder = MaterialAlertDialogBuilder(this).apply {
setOnCancelListener { dialogInterface -> setScannerActive(true) }
// Header
setTitle(R.string.addWithoutBarcode)
}
// Layout
val layout = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
}
val contentPadding = resources.getDimensionPixelSize(R.dimen.alert_dialog_content_padding)
val params = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
leftMargin = contentPadding
topMargin = contentPadding / 2
rightMargin = contentPadding
}
// Description
val currentTextview = TextView(this).apply {
text = getString(R.string.enter_card_id)
layoutParams = params
}
layout.addView(currentTextview)
//EditText with spacing
val input = EditText(this).apply {
inputType = InputType.TYPE_CLASS_TEXT
layoutParams = params
}
layout.addView(input)
// Set layout
builder.setView(layout).apply {
setPositiveButton(getString(R.string.ok)) { _, _ ->
val loyaltyCard = LoyaltyCard()
loyaltyCard.cardId = input.text.toString()
returnResult(ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard))
}
setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
dialog.cancel()
}
}
val dialog: AlertDialog = builder.create()
// Now that the dialog exists, we can bind something that affects the OK button
input.doOnTextChanged { text, _, _, _ ->
if (text.isNullOrEmpty()) {
input.error = getString(R.string.card_id_must_not_be_empty)
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
} else {
input.error = null
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = true
}
}
dialog.show()
// Disable button (must be done **after** dialog is shown to prevent crash
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
// Set focus on input field
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
input.requestFocus()
}
fun addManually() {
val builder = MaterialAlertDialogBuilder(this).apply {
setTitle(R.string.add_manually_warning_title)
setMessage(R.string.add_manually_warning_message)
setPositiveButton(R.string.continue_) { _, _ ->
val i = Intent(applicationContext, BarcodeSelectorActivity::class.java)
if (cardId != null) {
val b = Bundle()
b.putString(LoyaltyCard.BUNDLE_LOYALTY_CARD_CARD_ID, cardId)
i.putExtras(b)
}
manualAddLauncher.launch(i)
}
setNegativeButton(R.string.cancel) { _, _ -> setScannerActive(true) }
setOnCancelListener { _ -> setScannerActive(true) }
}
builder.show()
}
fun addFromImage() {
PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_IMAGE)
}
fun addFromPdf() {
PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_PDF)
}
fun addFromPkPass() {
PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_PKPASS)
}
private fun addFromImageOrFileAfterPermission(
mimeType: String,
launcher: ActivityResultLauncher<Intent>,
chooserText: Int,
errorMessage: Int
) {
val photoPickerIntent = Intent(Intent.ACTION_PICK)
photoPickerIntent.type = mimeType
val contentIntent = Intent(Intent.ACTION_GET_CONTENT)
contentIntent.type = mimeType
val chooserIntent = Intent.createChooser(photoPickerIntent, getString(chooserText))
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(contentIntent))
try {
launcher.launch(chooserIntent)
} catch (e: ActivityNotFoundException) {
setScannerActive(true)
Toast.makeText(applicationContext, errorMessage, Toast.LENGTH_LONG).show()
Log.e(TAG, "No activity found to handle intent", e)
}
}
fun onCaptureManagerError(errorMessage: String) {
if (mHasError) {
// We're already showing an error, ignore this new error
return
}
showCameraError(errorMessage, false)
}
private fun showCameraPermissionMissingText() {
showCameraError(getString(R.string.noCameraPermissionDirectToSystemSetting), true)
}
private fun showCameraError(message: String, setOnClick: Boolean) {
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorMessage.text = message
setCameraErrorState(true, setOnClick)
}
private fun hideCameraError() {
setCameraErrorState(false, false)
}
private fun setCameraErrorState(visible: Boolean, setOnClick: Boolean) {
mHasError = visible
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorClickableArea.setOnClickListener(
if (visible && setOnClick) { _ -> navigateToSystemPermissionSetting() }
else null
)
customBarcodeScannerBinding.cardInputContainer.setBackgroundColor(
if (visible) obtainThemeAttribute(com.google.android.material.R.attr.colorSurface)
else Color.TRANSPARENT
)
customBarcodeScannerBinding.cameraErrorLayout.root.visibility =
if (visible) View.VISIBLE else View.GONE
}
private fun scaleScreen() {
val displayMetrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(displayMetrics)
val screenHeight: Int = displayMetrics.heightPixels
val mediumSizePx: Float = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
MEDIUM_SCALE_FACTOR_DIP.toFloat(),
resources.displayMetrics
)
val shouldScaleSmaller = screenHeight < mediumSizePx
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorIcon.visibility =
if (shouldScaleSmaller) View.GONE else View.VISIBLE
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorTitle.visibility =
if (shouldScaleSmaller) View.GONE else View.VISIBLE
}
private fun obtainThemeAttribute(attribute: Int): Int {
val typedValue = TypedValue()
theme.resolveAttribute(attribute, typedValue, true)
return typedValue.data
}
private fun navigateToSystemPermissionSetting() {
val permissionIntent = Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", getPackageName(), null)
)
permissionIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(permissionIntent)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
onMockedRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun onMockedRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
val granted =
grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
if (requestCode == CaptureManager.getCameraPermissionReqCode()) {
if (granted) {
hideCameraError()
} else {
showCameraPermissionMissingText()
}
} else if (requestCode in listOf(
PERMISSION_SCAN_ADD_FROM_IMAGE,
PERMISSION_SCAN_ADD_FROM_PDF,
PERMISSION_SCAN_ADD_FROM_PKPASS
)
) {
if (granted) {
if (requestCode == PERMISSION_SCAN_ADD_FROM_IMAGE) {
addFromImageOrFileAfterPermission(
"image/*",
photoPickerLauncher,
R.string.addFromImage,
R.string.failedLaunchingPhotoPicker
)
} else if (requestCode == PERMISSION_SCAN_ADD_FROM_PDF) {
addFromImageOrFileAfterPermission(
"application/pdf",
pdfPickerLauncher,
R.string.addFromPdfFile,
R.string.failedLaunchingFileManager
)
} else {
addFromImageOrFileAfterPermission(
"application/*",
pkpassPickerLauncher,
R.string.addFromPkpass,
R.string.failedLaunchingFileManager
)
}
} else {
setScannerActive(true)
Toast.makeText(this, R.string.storageReadPermissionRequired, Toast.LENGTH_LONG)
.show()
}
}
}
}

View File

@@ -0,0 +1,29 @@
package protect.card_locker;
public class ThirdPartyInfo {
private final String mName;
private final String mUrl;
private final String mLicense;
public ThirdPartyInfo(String name, String url, String license) {
mName = name;
mUrl = url;
mLicense = license;
}
public String name() {
return mName;
}
public String url() {
return mUrl;
}
public String license() {
return mLicense;
}
public String toHtml() {
return String.format("<a href=\"%s\">%s</a> (%s)", url(), name(), license());
}
}

View File

@@ -1,23 +0,0 @@
package protect.card_locker
class ThirdPartyInfo(
private val mName: String,
private val mUrl: String,
private val mLicense: String
) {
fun name(): String {
return mName
}
fun url(): String {
return mUrl
}
fun license(): String {
return mLicense
}
fun toHtml(): String {
return String.format("<a href=\"%s\">%s</a> (%s)", url(), name(), license())
}
}

View File

@@ -0,0 +1,93 @@
package protect.card_locker;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.ColorUtils;
import androidx.core.view.WindowInsetsControllerCompat;
import com.google.android.material.color.MaterialColors;
import com.google.android.material.textview.MaterialTextView;
import com.yalantis.ucrop.UCropActivity;
public class UCropWrapper extends UCropActivity {
public static final String UCROP_TOOLBAR_TYPEFACE_STYLE = "ucop_toolbar_typeface_style";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Utils.applyWindowInsets(findViewById(android.R.id.content));
}
@Override
protected void onPostCreate(@Nullable Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
boolean darkMode = Utils.isDarkModeEnabled(this);
Window window = getWindow();
// setup status bar to look like the rest of the app
if (Build.VERSION.SDK_INT >= 23) {
if (window != null) {
View decorView = window.getDecorView();
WindowInsetsControllerCompat wic = new WindowInsetsControllerCompat(window, decorView);
wic.setAppearanceLightStatusBars(!darkMode);
}
} else {
// icons are always white back then
if (window != null && !darkMode) {
window.setStatusBarColor(ColorUtils.compositeColors(Color.argb(127, 0, 0, 0), window.getStatusBarColor()));
}
}
// find and check views that we wish to color modify
// for when we update ucrop or switch to another cropper
View check = findViewById(com.yalantis.ucrop.R.id.wrapper_controls);
if (check instanceof FrameLayout) {
FrameLayout controls = (FrameLayout) check;
check = findViewById(com.yalantis.ucrop.R.id.wrapper_states);
if (check instanceof LinearLayout) {
LinearLayout states = (LinearLayout) check;
for (int i = 0; i < controls.getChildCount(); i++) {
check = controls.getChildAt(i);
if (check instanceof AppCompatImageView) {
AppCompatImageView controlsBackgroundImage = (AppCompatImageView) check;
// everything gathered and are as expected, now perform color patching
Utils.patchColors(this);
int colorSurface = MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurface, ContextCompat.getColor(this, R.color.md_theme_light_surface));
int colorOnSurface = MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSurface, ContextCompat.getColor(this, R.color.md_theme_light_onSurface));
Drawable controlsBackgroundImageDrawable = controlsBackgroundImage.getBackground();
controlsBackgroundImageDrawable.mutate();
controlsBackgroundImageDrawable.setTint(darkMode ? colorOnSurface : colorSurface);
controlsBackgroundImage.setBackgroundDrawable(controlsBackgroundImageDrawable);
states.setBackgroundColor(darkMode ? colorSurface : colorOnSurface);
break;
}
}
}
}
// change toolbar font
check = findViewById(com.yalantis.ucrop.R.id.toolbar_title);
if (check instanceof MaterialTextView) {
MaterialTextView toolbarTextview = (MaterialTextView) check;
Intent intent = getIntent();
int style = intent.getIntExtra(UCROP_TOOLBAR_TYPEFACE_STYLE, -1);
if (style != -1) {
toolbarTextview.setTypeface(Typeface.defaultFromStyle(style));
}
}
}
}

View File

@@ -1,122 +0,0 @@
package protect.card_locker
import android.graphics.Color
import android.graphics.Typeface
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.children
import com.google.android.material.color.MaterialColors
import com.google.android.material.textview.MaterialTextView
import com.yalantis.ucrop.UCropActivity
class UCropWrapper : UCropActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Utils.applyWindowInsets(findViewById(android.R.id.content))
}
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
val darkMode = Utils.isDarkModeEnabled(this)
// setup status bar to look like the rest of the app
setupStatusBar(darkMode)
// find and check views that we wish to color modify
// for when we update ucrop or switch to another cropper
checkViews(darkMode)
// change toolbar font
changeToolbarFont()
}
private fun setupStatusBar(darkMode: Boolean) {
if (window == null) {
return
}
if (Build.VERSION.SDK_INT >= 23) {
val decorView = window.decorView
val wic = WindowInsetsControllerCompat(window, decorView)
wic.isAppearanceLightStatusBars = !darkMode
} else if (!darkMode) {
window.statusBarColor = ColorUtils.compositeColors(
Color.argb(127, 0, 0, 0),
window.statusBarColor
)
}
}
private fun checkViews(darkMode: Boolean) {
var view = findViewById<View?>(com.yalantis.ucrop.R.id.wrapper_controls)
if (view !is FrameLayout) {
return
}
val controls = view
view = findViewById(com.yalantis.ucrop.R.id.wrapper_states)
if (view !is LinearLayout) {
return
}
val states = view
controls.children.firstOrNull { it is AppCompatImageView }?.let {
// everything gathered and are as expected, now perform color patching
Utils.patchColors(this)
val colorSurface = MaterialColors.getColor(
this,
com.google.android.material.R.attr.colorSurface,
ContextCompat.getColor(
this,
R.color.md_theme_light_surface
)
)
val colorOnSurface = MaterialColors.getColor(
this,
com.google.android.material.R.attr.colorOnSurface,
ContextCompat.getColor(
this,
R.color.md_theme_light_onSurface
)
)
val controlsBackgroundImageDrawable = it.background
controlsBackgroundImageDrawable.mutate()
controlsBackgroundImageDrawable.setTint(
if (darkMode) {
colorOnSurface
} else {
colorSurface
}
)
it.background = controlsBackgroundImageDrawable
states.setBackgroundColor(
if (darkMode) {
colorSurface
} else {
colorOnSurface
}
)
}
}
private fun changeToolbarFont() {
val toolbar = findViewById<View?>(com.yalantis.ucrop.R.id.toolbar_title)
if (toolbar !is MaterialTextView) {
return
}
val style = intent.getIntExtra(UCROP_TOOLBAR_TYPEFACE_STYLE, -1)
if (style != -1) {
toolbar.setTypeface(Typeface.defaultFromStyle(style))
}
}
internal companion object {
const val UCROP_TOOLBAR_TYPEFACE_STYLE: String = "ucop_toolbar_typeface_style"
}
}

View File

@@ -87,10 +87,10 @@ import java.util.Currency;
import java.util.Date;
import java.util.EnumMap;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -118,7 +118,7 @@ public class Utils {
static final double LUMINANCE_MIDPOINT = 0.5;
static final int BITMAP_SIZE_SMALL = 512;
static final int BITMAP_SIZE_BIG = 1600;
static final int BITMAP_SIZE_BIG = 2048;
static public LetterBitmap generateIcon(Context context, LoyaltyCard loyaltyCard, boolean forShortcut) {
return generateIcon(context, loyaltyCard.store, loyaltyCard.headerColor, forShortcut);
@@ -143,7 +143,7 @@ public class Utils {
int pixelSize = context.getResources().getDimensionPixelSize(R.dimen.tileLetterImageSize);
if (backgroundColor == null) {
backgroundColor = LetterBitmap.Companion.getDefaultColor(context, store);
backgroundColor = LetterBitmap.getDefaultColor(context, store);
}
return new LetterBitmap(context, store, store,
@@ -228,58 +228,6 @@ public class Utils {
return parseResultList;
}
static public List<ParseResult> retrieveBarcodesFromPkPasses(Context context, Uri uri) {
Log.i(TAG, "Received Pkpasses file with possible barcode");
if (uri == null) {
Log.e(TAG, "Pkpasses did not contain any data");
Toast.makeText(context, R.string.errorReadingFile, Toast.LENGTH_LONG).show();
return new ArrayList<>();
}
PkpassesParser pkpassesParser;
try {
pkpassesParser = new PkpassesParser(context, uri);
} catch (Exception e) {
Log.e(TAG, "Error reading pkpasses file", e);
Toast.makeText(context, R.string.errorReadingFile, Toast.LENGTH_LONG).show();
return new ArrayList<>();
}
List<ParseResult> parseResultList = new ArrayList<>();
int i = 0;
for (PkpassParser pkpassParser : pkpassesParser.getPkpassParsers()) {
ParseResult parseResult;
List<String> locales = pkpassParser.listLocales();
if (locales.isEmpty()) {
try {
parseResult = new ParseResult(ParseResultType.FULL, pkpassParser.toLoyaltyCard(null));
} catch (Exception e) {
Log.e(TAG, "Error calling toLoyaltyCard on pkpass file", e);
Toast.makeText(context, R.string.errorReadingFile, Toast.LENGTH_LONG).show();
return new ArrayList<>();
}
parseResult.setNote(String.format(context.getString(R.string.cardWithNumber), i+1));
parseResultList.add(parseResult);
} else {
for (String locale : locales) {
try {
parseResult = new ParseResult(ParseResultType.FULL, pkpassParser.toLoyaltyCard(locale));
} catch (Exception e) {
Log.e(TAG, "Error calling toLoyaltyCard on pkpass file", e);
Toast.makeText(context, R.string.errorReadingFile, Toast.LENGTH_LONG).show();
return new ArrayList<>();
}
parseResult.setNote(String.format(context.getString(R.string.cardWithNumberAndLocale), i+1, locale));
parseResultList.add(parseResult);
}
}
i++;
}
return parseResultList;
}
static public List<ParseResult> retrieveBarcodesFromPdf(Context context, Uri uri) {
Log.i(TAG, "Received PDF file with possible barcode");
if (uri == null) {
@@ -371,19 +319,7 @@ public class Utils {
}
if (requestCode == Utils.BARCODE_IMPORT_FROM_PKPASS_FILE) {
Uri intentData = intent.getData();
if (intentData == null) {
Log.e(TAG, "Uri did not contain any data");
Toast.makeText(context, R.string.errorReadingFile, Toast.LENGTH_LONG).show();
return new ArrayList<>();
}
if (Objects.equals(context.getContentResolver().getType(intentData), "application/vnd.apple.pkpasses")) {
return retrieveBarcodesFromPkPasses(context, intentData);
}
return retrieveBarcodesFromPkPass(context, intentData);
return retrieveBarcodesFromPkPass(context, intent.getData());
}
if (requestCode == Utils.BARCODE_SCAN || requestCode == Utils.SELECT_BARCODE_REQUEST) {
@@ -914,7 +850,7 @@ public class Utils {
public static File copyToTempFile(Context context, InputStream input, String name) throws IOException {
File file = createTempFile(context, name);
try (FileOutputStream out = new FileOutputStream(file)) {
try (input; FileOutputStream out = new FileOutputStream(file)) {
byte[] buf = new byte[4096];
int len;
while ((len = input.read(buf)) != -1) {
@@ -963,9 +899,31 @@ public class Utils {
// replace colors in the current theme
public static void patchColors(AppCompatActivity activity) {
Settings settings = new Settings(activity);
String color = settings.getColor();
Resources.Theme theme = activity.getTheme();
DynamicColors.applyToActivityIfAvailable(activity);
Resources resources = activity.getResources();
if (color.equals(resources.getString(R.string.settings_key_pink_theme))) {
theme.applyStyle(R.style.pink, true);
} else if (color.equals(resources.getString(R.string.settings_key_magenta_theme))) {
theme.applyStyle(R.style.magenta, true);
} else if (color.equals(resources.getString(R.string.settings_key_violet_theme))) {
theme.applyStyle(R.style.violet, true);
} else if (color.equals(resources.getString(R.string.settings_key_blue_theme))) {
theme.applyStyle(R.style.blue, true);
} else if (color.equals(resources.getString(R.string.settings_key_sky_blue_theme))) {
theme.applyStyle(R.style.skyblue, true);
} else if (color.equals(resources.getString(R.string.settings_key_green_theme))) {
theme.applyStyle(R.style.green, true);
} else if (color.equals(resources.getString(R.string.settings_key_brown_theme))) {
theme.applyStyle(R.style.brown, true);
} else if (color.equals(resources.getString(R.string.settings_key_catima_theme))) {
// catima theme is AppTheme itself, no dynamic colors nor applyStyle
} else {
// final catch all in case of invalid theme value from older versions
// also handles R.string.settings_key_system_theme
DynamicColors.applyToActivityIfAvailable(activity);
}
if (isDarkModeEnabled(activity) && settings.getOledDark()) {
theme.applyStyle(R.style.DarkBackground, true);
@@ -1107,7 +1065,7 @@ public class Utils {
}
public static int getHeaderColor(Context context, LoyaltyCard loyaltyCard) {
return loyaltyCard.headerColor != null ? loyaltyCard.headerColor : LetterBitmap.Companion.getDefaultColor(context, loyaltyCard.store);
return loyaltyCard.headerColor != null ? loyaltyCard.headerColor : LetterBitmap.getDefaultColor(context, loyaltyCard.store);
}
public static String checksum(InputStream input) throws IOException {

View File

@@ -0,0 +1,9 @@
package protect.card_locker.async;
import java.util.concurrent.Callable;
public interface CompatCallable<T> extends Callable<T> {
void onPostExecute(Object result);
void onPreExecute();
}

View File

@@ -1,9 +0,0 @@
package protect.card_locker.async
import java.util.concurrent.Callable
interface CompatCallable<T> : Callable<T?> {
fun onPostExecute(result: Any?)
fun onPreExecute()
}

View File

@@ -1,97 +0,0 @@
package protect.card_locker.compose
import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import protect.card_locker.OpenWebLinkHandler
import protect.card_locker.R
@Composable
fun CatimaAboutSection(
title: String,
message: String,
modifier: Modifier = Modifier,
onClickUrl: String? = null,
onClickDialogText: AnnotatedString? = null,
) {
val activity = LocalActivity.current
val openDialog = remember { mutableStateOf(false) }
Row(
modifier = modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.clickable {
if (onClickDialogText != null) {
openDialog.value = true
} else if (onClickUrl != null) {
OpenWebLinkHandler().openBrowser(activity, onClickUrl)
}
}
) {
Column(modifier = Modifier.weight(1F)) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium
)
Text(text = message)
}
Text(modifier = Modifier.align(Alignment.CenterVertically),
text = ">",
style = MaterialTheme.typography.bodyMedium
)
}
if (openDialog.value && onClickDialogText != null) {
AlertDialog(
icon = {},
title = {
Text(text = title)
},
text = {
Text(
text = onClickDialogText,
modifier = Modifier.verticalScroll(rememberScrollState())
)
},
onDismissRequest = {
openDialog.value = false
},
confirmButton = {
TextButton(
onClick = {
openDialog.value = false
}
) {
Text(stringResource(R.string.ok))
}
},
dismissButton = {
if (onClickUrl != null) {
TextButton(
onClick = {
OpenWebLinkHandler().openBrowser(activity, onClickUrl)
}
) {
Text(stringResource(R.string.view_online))
}
}
}
)
}
}

View File

@@ -1,34 +0,0 @@
package protect.card_locker.compose
import androidx.activity.OnBackPressedDispatcher
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import protect.card_locker.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CatimaTopAppBar(title: String, onBackPressedDispatcher: OnBackPressedDispatcher?) {
TopAppBar(
modifier = Modifier.testTag("topbar_catima"),
title = { Text(text = title) },
navigationIcon = {
if (onBackPressedDispatcher != null) {
IconButton(onClick = { onBackPressedDispatcher.onBackPressed() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back)
)
}
}
}
)
}

View File

@@ -1,51 +0,0 @@
package protect.card_locker.compose.theme
import android.os.Build
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import protect.card_locker.R
import protect.card_locker.preferences.Settings
@Composable
fun CatimaTheme(content: @Composable () -> Unit) {
val context = LocalContext.current
val settings = Settings(context)
val isDynamicColorSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val lightTheme = if (isDynamicColorSupported) {
dynamicLightColorScheme(context)
} else {
lightColorScheme(primary = colorResource(id = R.color.md_theme_light_primary))
}
var darkTheme = if (isDynamicColorSupported) {
dynamicDarkColorScheme(context)
} else {
darkColorScheme(primary = colorResource(id = R.color.md_theme_dark_primary))
}
if (settings.oledDark) {
darkTheme = darkTheme.copy(background = Color.Black)
}
val colorScheme = when (settings.theme) {
AppCompatDelegate.MODE_NIGHT_NO -> lightTheme
AppCompatDelegate.MODE_NIGHT_YES -> darkTheme
else -> if (isSystemInDarkTheme()) darkTheme else lightTheme
}
MaterialTheme(
colorScheme = colorScheme,
content = content
)
}

View File

@@ -52,7 +52,6 @@ public class CardsContentProvider extends ContentProvider {
LoyaltyCardDbIds.CARD_ID,
LoyaltyCardDbIds.BARCODE_ID,
LoyaltyCardDbIds.BARCODE_TYPE,
// FIXME: Expose BARCODE_ENCODING but without ever exposing the null value (so apps using this don't have to guess)
LoyaltyCardDbIds.STAR_STATUS,
LoyaltyCardDbIds.LAST_USED,
LoyaltyCardDbIds.ARCHIVE_STATUS,

View File

@@ -134,7 +134,6 @@ public class CatimaExporter implements Exporter {
DBHelper.LoyaltyCardDbIds.CARD_ID,
DBHelper.LoyaltyCardDbIds.BARCODE_ID,
DBHelper.LoyaltyCardDbIds.BARCODE_TYPE,
DBHelper.LoyaltyCardDbIds.BARCODE_ENCODING,
DBHelper.LoyaltyCardDbIds.HEADER_COLOR,
DBHelper.LoyaltyCardDbIds.STAR_STATUS,
DBHelper.LoyaltyCardDbIds.LAST_USED,
@@ -155,7 +154,6 @@ public class CatimaExporter implements Exporter {
card.cardId,
card.barcodeId,
card.barcodeType != null ? card.barcodeType.name() : "",
card.barcodeEncoding != null ? card.barcodeEncoding.name() : "",
card.headerColor,
card.starStatus,
card.lastUsed,

View File

@@ -20,7 +20,6 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Currency;
@@ -128,10 +127,10 @@ public class CatimaImporter implements Importer {
LoyaltyCard existing = DBHelper.getLoyaltyCard(context, database, card.id);
if (existing == null) {
DBHelper.insertLoyaltyCard(database, card.id, card.store, card.note, card.validFrom, card.expiry, card.balance, card.balanceType,
card.cardId, card.barcodeId, card.barcodeType, card.barcodeEncoding, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus);
card.cardId, card.barcodeId, card.barcodeType, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus);
} else if (!isDuplicate(context, existing, card, existingImages, imageChecksums)) {
long newId = DBHelper.insertLoyaltyCard(database, card.store, card.note, card.validFrom, card.expiry, card.balance, card.balanceType,
card.cardId, card.barcodeId, card.barcodeType, card.barcodeEncoding, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus);
card.cardId, card.barcodeId, card.barcodeType, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus);
idMap.put(card.id, (int) newId);
}
}
@@ -459,12 +458,6 @@ public class CatimaImporter implements Importer {
barcodeType = CatimaBarcode.fromName(unparsedBarcodeType);
}
Charset barcodeEncoding = null;
String unparsedBarcodeEncoding = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.BARCODE_ENCODING, record, "");
if (!unparsedBarcodeEncoding.isEmpty()) {
barcodeEncoding = Charset.forName(unparsedBarcodeEncoding);
}
Integer headerColor = null;
try {
headerColor = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIds.HEADER_COLOR, record);
@@ -508,7 +501,6 @@ public class CatimaImporter implements Importer {
cardId,
barcodeId,
barcodeType,
barcodeEncoding,
headerColor,
starStatus,
lastUsed,

View File

@@ -0,0 +1,8 @@
package protect.card_locker.importexport;
public enum DataFormat {
Catima,
Fidme,
Stocard,
VoucherVault;
}

View File

@@ -1,7 +0,0 @@
package protect.card_locker.importexport
enum class DataFormat {
Catima,
Fidme,
VoucherVault
}

View File

@@ -0,0 +1,20 @@
package protect.card_locker.importexport;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import java.io.IOException;
import java.io.OutputStream;
/**
* Interface for a class which can export the contents of the database
* in a given format.
*/
public interface Exporter {
/**
* Export the database to the output stream in a given format.
*
* @throws IOException
*/
void exportData(Context context, SQLiteDatabase database, OutputStream output, char[] password) throws IOException, InterruptedException;
}

View File

@@ -1,25 +0,0 @@
package protect.card_locker.importexport
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import java.io.IOException
import java.io.OutputStream
/**
* Interface for a class which can export the contents of the database
* in a given format.
*/
interface Exporter {
/**
* Export the database to the output stream in a given format.
*
* @throws IOException, InterruptedException
*/
@Throws(IOException::class, InterruptedException::class)
fun exportData(
context: Context,
database: SQLiteDatabase,
output: OutputStream,
password: CharArray
)
}

View File

@@ -160,7 +160,6 @@ public class FidmeImporter implements Importer {
cardId,
null,
barcodeType,
null,
headerColor,
starStatus,
Utils.getUnixTime(),
@@ -182,7 +181,7 @@ public class FidmeImporter implements Importer {
for (LoyaltyCard card : data.cards) {
// Do not use card.id which is set to -1
DBHelper.insertLoyaltyCard(database, card.store, card.note, card.validFrom, card.expiry, card.balance, card.balanceType,
card.cardId, card.barcodeId, card.barcodeType, card.barcodeEncoding, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus);
card.cardId, card.barcodeId, card.barcodeType, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus);
}
}
}

View File

@@ -0,0 +1,7 @@
package protect.card_locker.importexport;
public enum ImportExportResultType {
Success,
GenericFailure,
BadPassword;
}

View File

@@ -1,7 +0,0 @@
package protect.card_locker.importexport
enum class ImportExportResultType {
Success,
GenericFailure,
BadPassword
}

View File

@@ -0,0 +1,27 @@
package protect.card_locker.importexport;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import org.json.JSONException;
import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import protect.card_locker.FormatException;
/**
* Interface for a class which can import the contents of a stream
* into the database.
*/
public interface Importer {
/**
* Import data from the input stream in a given format into
* the database.
*
* @throws IOException
* @throws FormatException
*/
void importData(Context context, SQLiteDatabase database, File inputFile, char[] password) throws IOException, FormatException, InterruptedException, JSONException, ParseException;
}

View File

@@ -1,39 +0,0 @@
package protect.card_locker.importexport
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import org.json.JSONException
import protect.card_locker.FormatException
import java.io.File
import java.io.IOException
import java.text.ParseException
/**
* Interface for a class which can import the contents of a stream
* into the database.
*/
interface Importer {
/**
* Import data from the input stream in a given format into
* the database.
*
* @throws IOException
* @throws FormatException
* @throws InterruptedException
* @throws JSONException
* @throws ParseException
*/
@Throws(
IOException::class,
FormatException::class,
InterruptedException::class,
JSONException::class,
ParseException::class
)
fun importData(
context: Context,
database: SQLiteDatabase,
inputFile: File,
password: CharArray
)
}

View File

@@ -37,6 +37,9 @@ public class MultiFormatImporter {
case Fidme:
importer = new FidmeImporter();
break;
case Stocard:
importer = new StocardImporter();
break;
case VoucherVault:
importer = new VoucherVaultImporter();
break;

View File

@@ -0,0 +1,428 @@
package protect.card_locker.importexport;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Bitmap;
import android.util.Log;
import androidx.annotation.NonNull;
import com.google.zxing.BarcodeFormat;
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.io.inputstream.ZipInputStream;
import net.lingala.zip4j.model.FileHeader;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import protect.card_locker.CatimaBarcode;
import protect.card_locker.DBHelper;
import protect.card_locker.FormatException;
import protect.card_locker.ImageLocationType;
import protect.card_locker.LoyaltyCard;
import protect.card_locker.R;
import protect.card_locker.Utils;
import protect.card_locker.ZipUtils;
/**
* Class for importing a database from CSV (Comma Separate Values)
* formatted data.
* <p>
* The database's loyalty cards are expected to appear in the CSV data.
* A header is expected for the each table showing the names of the columns.
*/
public class StocardImporter implements Importer {
public static class StocardProvider {
public String name = null;
public String barcodeFormat = null;
public Bitmap logo = null;
}
public static class StocardRecord {
public String providerId = null;
public String store = null;
public String label = null;
public String note = null;
public String cardId = null;
public String barcodeType = null;
public Long lastUsed = null;
public Bitmap frontImage = null;
public Bitmap backImage = null;
@NonNull
@Override
public String toString() {
return String.format(
"StocardRecord{%n providerId=%s,%n store=%s,%n label=%s,%n note=%s,%n cardId=%s,%n"
+ " barcodeType=%s,%n lastUsed=%s,%n frontImage=%s,%n backImage=%s%n}",
this.providerId,
this.store,
this.label,
this.note,
this.cardId,
this.barcodeType,
this.lastUsed,
this.frontImage,
this.backImage
);
}
}
public static class ZIPData {
public final Map<String, StocardRecord> cards;
public final Map<String, StocardProvider> providers;
ZIPData(final Map<String, StocardRecord> cards, final Map<String, StocardProvider> providers) {
this.cards = cards;
this.providers = providers;
}
}
public static class ImportedData {
public final List<LoyaltyCard> cards;
public final Map<Integer, Map<ImageLocationType, Bitmap>> images;
ImportedData(final List<LoyaltyCard> cards, final Map<Integer, Map<ImageLocationType, Bitmap>> images) {
this.cards = cards;
this.images = images;
}
}
public static final String PROVIDER_PREFIX = "/loyalty-card-providers/";
private static final String TAG = "Catima";
public void importData(Context context, SQLiteDatabase database, File inputFile, char[] password) throws IOException, FormatException, JSONException, ParseException {
ZIPData zipData = new ZIPData(new HashMap<>(), new HashMap<>());
final CSVParser parser = new CSVParser(new InputStreamReader(context.getResources().openRawResource(R.raw.stocard_stores), StandardCharsets.UTF_8), CSVFormat.RFC4180.builder().setHeader().build());
try {
for (CSVRecord record : parser) {
StocardProvider provider = new StocardProvider();
provider.name = record.get("name").trim();
provider.barcodeFormat = record.get("barcodeFormat").trim();
zipData.providers.put(record.get("_id").trim(), provider);
}
parser.close();
} catch (IllegalArgumentException | IllegalStateException e) {
throw new FormatException("Issue parsing CSV data", e);
}
ZipFile zipFile = new ZipFile(inputFile, password);
zipData = importZIP(zipFile, zipData);
zipFile.close();
if (zipData.cards.keySet().size() == 0) {
throw new FormatException("Couldn't find any loyalty cards in this Stocard export.");
}
ImportedData importedData = importLoyaltyCardHashMap(context, zipData);
saveAndDeduplicate(context, database, importedData);
}
public ZIPData importZIP(ZipFile zipFile, final ZIPData zipData) throws IOException, FormatException, JSONException {
Map<String, StocardRecord> cards = zipData.cards;
Map<String, StocardProvider> providers = zipData.providers;
String[] customProvidersBaseName = null;
String[] cardBaseName = null;
String customProviderId = "";
String cardName = "";
for (FileHeader fileHeader : zipFile.getFileHeaders()) {
String fileName = fileHeader.getFileName();
String[] nameParts = fileName.split("/");
if (nameParts.length < 2) {
continue;
}
String userId = nameParts[1];
ZipInputStream zipInputStream = zipFile.getInputStream(fileHeader);
if (customProvidersBaseName == null) {
// FIXME: can we use the points-account/statement/content.json balance info somehow?
/*
Known files:
extracts/<user-UUID>/users/<user-UUID>/
analytics-properties/content.json
devices/<device-UUID>/
analytics-properties/content.json
content.json
ip-location-wifi/content.json
enabled-regions/<UUID>/content.json
loyalty-card-custom-providers/<provider-UUID>/content.json - custom providers
loyalty-cards/<card-UUID>/
card-linked-coupons/accounts/default/
content.json
user-coupons/<UUID>/content.json
content.json - card itself
images/back.png - back image (legacy)
images/back/back.jpg - back image
images/back/content.json
images/front.png - front image (legacy)
images/front/content.json
images/front/front.jpg - front image
notes/default/content.json - note
points-account/
content.json
statement/content.json
usages/<UUID>/content.json - timestamps
usage-statistics/content.json - timestamps
reward-program-balances/<UUID>/content.json
*/
customProvidersBaseName = new String[]{
"extracts",
userId,
"users",
userId,
"loyalty-card-custom-providers"
};
cardBaseName = new String[]{
"extracts",
userId,
"users",
userId,
"loyalty-cards"
};
}
if (startsWith(nameParts, customProvidersBaseName, 1)) {
// Extract providerId
customProviderId = nameParts[customProvidersBaseName.length];
StocardProvider provider = providers.get(customProviderId);
if (provider == null) {
provider = new StocardProvider();
providers.put(customProviderId, provider);
}
// Name file
if (fileName.endsWith(customProviderId + "/content.json")) {
JSONObject jsonObject = ZipUtils.readJSON(zipInputStream);
provider.name = jsonObject.getString("name");
} else if (fileName.endsWith("logo.png")) {
provider.logo = ZipUtils.readImage(zipInputStream);
} else if (!fileName.endsWith("/")) {
Log.d(TAG, "Unknown or unused loyalty-card-custom-providers file " + fileName + ", skipping...");
}
} else if (startsWith(nameParts, cardBaseName, 1)) {
// Extract cardName
cardName = nameParts[cardBaseName.length];
StocardRecord record = cards.get(cardName);
if (record == null) {
record = new StocardRecord();
cards.put(cardName, record);
}
// This is the card itself
if (fileName.endsWith(cardName + "/content.json")) {
JSONObject jsonObject = ZipUtils.readJSON(zipInputStream);
record.cardId = jsonObject.getString("input_id");
if (jsonObject.has("input_provider_name")) {
record.store = jsonObject.getString("input_provider_name");
}
if (jsonObject.has("label")) {
String label = jsonObject.getString("label");
if (!label.isBlank()) {
record.label = label;
}
}
// Provider ID can be either custom or not, extract whatever version is relevant
String customProviderPrefix = "/users/" + userId + "/loyalty-card-custom-providers/";
String providerId = jsonObject
.getJSONObject("input_provider_reference")
.getString("identifier");
if (providerId.startsWith(customProviderPrefix)) {
providerId = providerId.substring(customProviderPrefix.length());
} else if (providerId.startsWith(PROVIDER_PREFIX)) {
providerId = providerId.substring(PROVIDER_PREFIX.length());
} else {
throw new FormatException("Unsupported provider ID format: " + providerId);
}
record.providerId = providerId;
if (jsonObject.has("input_barcode_format")) {
record.barcodeType = jsonObject.getString("input_barcode_format");
}
} else if (fileName.endsWith("notes/default/content.json")) {
record.note = ZipUtils.readJSON(zipInputStream).getString("content");
} else if (fileName.endsWith("usage-statistics/content.json")) {
JSONArray usages = ZipUtils.readJSON(zipInputStream).getJSONArray("usages");
for (int i = 0; i < usages.length(); i++) {
JSONObject lastUsedObject = usages.getJSONObject(i);
String lastUsedString = lastUsedObject.getJSONObject("time").getString("value");
long timeStamp = Instant.parse(lastUsedString).getEpochSecond();
if (record.lastUsed == null || timeStamp > record.lastUsed) {
record.lastUsed = timeStamp;
}
}
} else if (fileName.matches(".*/usages/[^/]+/content.json")) {
JSONObject lastUsedObject = ZipUtils.readJSON(zipInputStream);
String lastUsedString = lastUsedObject.getJSONObject("time").getString("value");
long timeStamp = Instant.parse(lastUsedString).getEpochSecond();
if (record.lastUsed == null || timeStamp > record.lastUsed) {
record.lastUsed = timeStamp;
}
} else if (fileName.endsWith("/images/front.png") || fileName.endsWith("/images/front/front.jpg")) {
record.frontImage = ZipUtils.readImage(zipInputStream);
} else if (fileName.endsWith("/images/back.png") || fileName.endsWith("/images/back/back.jpg")) {
record.backImage = ZipUtils.readImage(zipInputStream);
} else if (!fileName.endsWith("/")) {
Log.d(TAG, "Unknown or unused loyalty-cards file " + fileName + ", skipping...");
}
} else if (!fileName.endsWith("/")) {
Log.d(TAG, "Unknown or unused file " + fileName + ", skipping...");
}
zipInputStream.close();
}
return new ZIPData(cards, providers);
}
public ImportedData importLoyaltyCardHashMap(Context context, final ZIPData zipData) throws FormatException {
ImportedData importedData = new ImportedData(new ArrayList<>(), new HashMap<>());
int tempID = 0;
List<String> cardKeys = new ArrayList<>(zipData.cards.keySet());
Collections.sort(cardKeys);
for (String key : cardKeys) {
StocardRecord record = zipData.cards.get(key);
if (record.providerId == null) {
Log.d(TAG, "Missing providerId for card " + record + ", ignoring...");
continue;
}
if (record.cardId == null) {
throw new FormatException("No card ID listed, but is required");
}
StocardProvider provider = zipData.providers.get(record.providerId);
// Read store from card, if not available (old export), fall back to providerData
String store = record.store != null ? record.store : provider != null ? provider.name : record.providerId;
String note = record.note != null ? record.note : "";
String barcodeTypeString = record.barcodeType != null ? record.barcodeType : provider != null ? provider.barcodeFormat : null;
if (record.label != null && !record.label.equals(store) && !record.label.equals(note)) {
note = note.isEmpty() ? record.label : note + "\n" + record.label;
}
CatimaBarcode barcodeType = null;
if (barcodeTypeString != null && !barcodeTypeString.isEmpty()) {
if (barcodeTypeString.equals("RSS_DATABAR_EXPANDED")) {
barcodeType = CatimaBarcode.fromBarcode(BarcodeFormat.RSS_EXPANDED);
} else if (barcodeTypeString.equals("GS1_128")) {
barcodeType = CatimaBarcode.fromBarcode(BarcodeFormat.CODE_128);
} else {
barcodeType = CatimaBarcode.fromName(barcodeTypeString);
}
}
int headerColor = Utils.getRandomHeaderColor(context);
if (provider != null && provider.logo != null) {
headerColor = Utils.getHeaderColorFromImage(provider.logo, headerColor);
}
long lastUsed = record.lastUsed != null ? record.lastUsed : Utils.getUnixTime();
LoyaltyCard card = new LoyaltyCard(
tempID,
store,
note,
null,
null,
BigDecimal.valueOf(0),
null,
record.cardId,
null,
barcodeType,
headerColor,
0,
lastUsed,
DBHelper.DEFAULT_ZOOM_LEVEL,
DBHelper.DEFAULT_ZOOM_LEVEL_WIDTH,
0,
null,
null,
null,
null,
null,
null
);
importedData.cards.add(card);
Map<ImageLocationType, Bitmap> images = new HashMap<>();
if (provider != null && provider.logo != null) {
images.put(ImageLocationType.icon, provider.logo);
}
if (record.frontImage != null) {
images.put(ImageLocationType.front, record.frontImage);
}
if (record.backImage != null) {
images.put(ImageLocationType.back, record.backImage);
}
importedData.images.put(tempID, images);
tempID++;
}
return importedData;
}
public void saveAndDeduplicate(Context context, SQLiteDatabase database, final ImportedData data) throws IOException {
// This format does not have IDs that can cause conflicts
// Proper deduplication for all formats will be implemented later
for (LoyaltyCard card : data.cards) {
// card.id is temporary and only used to index the images Map
long id = DBHelper.insertLoyaltyCard(database, card.store, card.note, card.validFrom, card.expiry, card.balance, card.balanceType,
card.cardId, card.barcodeId, card.barcodeType, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus);
for (Map.Entry<ImageLocationType, Bitmap> entry : data.images.get(card.id).entrySet()) {
Utils.saveCardImage(context, entry.getValue(), (int) id, entry.getKey());
}
}
}
private boolean startsWith(String[] full, String[] start, int minExtraLength) {
if (full.length - minExtraLength < start.length) {
return false;
}
for (int i = 0; i < start.length; i++) {
if (!start[i].contentEquals(full[i])) {
return false;
}
}
return true;
}
}

View File

@@ -162,7 +162,6 @@ public class VoucherVaultImporter implements Importer {
cardId,
null,
barcodeType,
null,
headerColor,
0,
Utils.getUnixTime(),
@@ -187,7 +186,7 @@ public class VoucherVaultImporter implements Importer {
for (LoyaltyCard card : data.cards) {
// Do not use card.id which is set to -1
DBHelper.insertLoyaltyCard(database, card.store, card.note, card.validFrom, card.expiry, card.balance, card.balanceType,
card.cardId, card.barcodeId, card.barcodeType, card.barcodeEncoding, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus);
card.cardId, card.barcodeId, card.barcodeType, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus);
}
}
}

View File

@@ -74,6 +74,10 @@ public class Settings {
return getBoolean(R.string.settings_key_display_barcode_max_brightness, true);
}
public String getCardViewOrientation() {
return getString(R.string.settings_key_card_orientation, getResString(R.string.settings_key_follow_system_orientation));
}
public boolean getKeepScreenOn() {
return getBoolean(R.string.settings_key_keep_screen_on, true);
}
@@ -90,6 +94,10 @@ public class Settings {
return getBoolean(R.string.settings_key_oled_dark, false);
}
public String getColor() {
return getString(R.string.setting_key_theme_color, mContext.getResources().getString(R.string.settings_key_system_theme));
}
public int getPreferredColumnCount() {
var defaultSymbol = mContext.getResources().getString(R.string.settings_key_automatic_column_count);
var defaultColumnCount = mContext.getResources().getInteger(R.integer.main_view_card_columns);

View File

@@ -0,0 +1,206 @@
package protect.card_locker.preferences;
import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.view.MenuItem;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.widget.Toolbar;
import androidx.core.os.LocaleListCompat;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import com.google.android.material.color.DynamicColors;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.stream.Collectors;
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;
public class SettingsActivity extends CatimaAppCompatActivity {
private SettingsActivityBinding binding;
private final static String RELOAD_MAIN_STATE = "mReloadMain";
private SettingsFragment fragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = SettingsActivityBinding.inflate(getLayoutInflater());
setTitle(R.string.settings);
setContentView(binding.getRoot());
Utils.applyWindowInsets(binding.getRoot());
Toolbar toolbar = binding.toolbar;
setSupportActionBar(toolbar);
enableToolbarBackButton();
// Display the fragment as the main content.
fragment = new SettingsFragment();
getSupportFragmentManager().beginTransaction()
.replace(R.id.settings_container, fragment)
.commit();
// restore reload main state
if (savedInstanceState != null) {
fragment.mReloadMain = savedInstanceState.getBoolean(RELOAD_MAIN_STATE);
}
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
finishSettingsActivity();
}
});
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(RELOAD_MAIN_STATE, fragment.mReloadMain);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
finishSettingsActivity();
return true;
}
return super.onOptionsItemSelected(item);
}
private void finishSettingsActivity() {
if (fragment.mReloadMain) {
Intent intent = new Intent();
intent.putExtra(MainActivity.RESTART_ACTIVITY_INTENT, true);
setResult(Activity.RESULT_OK, intent);
} else {
setResult(Activity.RESULT_OK);
}
finish();
}
public static class SettingsFragment extends PreferenceFragmentCompat {
private static final String DIALOG_FRAGMENT_TAG = "SettingsFragment";
public boolean mReloadMain;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preferences);
// Show pretty names and summaries
ListPreference themePreference = findPreference(getResources().getString(R.string.settings_key_theme));
assert themePreference != null;
themePreference.setOnPreferenceChangeListener((preference, o) -> {
if (o.toString().equals(getResources().getString(R.string.settings_key_light_theme))) {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
} else if (o.toString().equals(getResources().getString(R.string.settings_key_dark_theme))) {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
} else {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
}
return true;
});
ListPreference themeColorPreference = findPreference(getResources().getString(R.string.setting_key_theme_color));
assert themeColorPreference != null;
themeColorPreference.setOnPreferenceChangeListener((preference, o) -> {
refreshActivity(true);
return true;
});
if (!DynamicColors.isDynamicColorAvailable()) {
themeColorPreference.setEntryValues(R.array.color_values_no_dynamic);
themeColorPreference.setEntries(R.array.color_value_strings_no_dynamic);
}
Preference oledDarkPreference = findPreference(getResources().getString(R.string.settings_key_oled_dark));
assert oledDarkPreference != null;
oledDarkPreference.setOnPreferenceChangeListener((preference, newValue) -> {
refreshActivity(true);
return true;
});
ListPreference localePreference = findPreference(getResources().getString(R.string.settings_key_locale));
assert localePreference != null;
CharSequence[] entryValues = localePreference.getEntryValues();
List<CharSequence> entries = new ArrayList<>();
for (CharSequence entry : entryValues) {
if (entry.length() == 0) {
entries.add(getResources().getString(R.string.settings_system_locale));
} else {
Locale entryLocale = Utils.stringToLocale(entry.toString());
entries.add(entryLocale.getDisplayName(entryLocale));
}
}
localePreference.setEntries(entries.toArray(new CharSequence[entryValues.length]));
// Make locale picker preference in sync with system settings
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Locale sysLocale = AppCompatDelegate.getApplicationLocales().get(0);
if (sysLocale == null) {
// Corresponds to "System"
localePreference.setValue("");
} else {
// Need to set preference's value to one of localePreference.getEntryValues() to match the locale.
// Locale.toLanguageTag() theoretically should be one of the values in localePreference.getEntryValues()...
// But it doesn't work for some locales. so trying something more heavyweight.
// Obtain all locales supported by the app.
List<Locale> appLocales = Arrays.stream(localePreference.getEntryValues())
.map(Objects::toString)
.map(Utils::stringToLocale)
.collect(Collectors.toList());
// Get the app locale that best matches the system one
Locale bestMatchLocale = Utils.getBestMatchLocale(appLocales, sysLocale);
// Get its index in supported locales
int index = appLocales.indexOf(bestMatchLocale);
// Set preference value to entry value at that index
localePreference.setValue(localePreference.getEntryValues()[index].toString());
}
}
localePreference.setOnPreferenceChangeListener((preference, newValue) -> {
// See corresponding comment in Utils.updateBaseContextLocale for Android 6- notes
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
refreshActivity(true);
return true;
}
String newLocale = (String) newValue;
// If newLocale is empty, that means "System" was selected
AppCompatDelegate.setApplicationLocales(newLocale.isEmpty() ? LocaleListCompat.getEmptyLocaleList() : LocaleListCompat.create(Utils.stringToLocale(newLocale)));
return true;
});
// Disable content provider on SDK < 23 since dangerous permissions
// are granted at install-time
Preference contentProviderReadPreference = findPreference(getResources().getString(R.string.settings_key_allow_content_provider_read));
assert contentProviderReadPreference != null;
contentProviderReadPreference.setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M);
}
private void refreshActivity(boolean reloadMain) {
mReloadMain = reloadMain || mReloadMain;
Activity activity = getActivity();
if (activity != null) {
activity.recreate();
}
}
}
}

View File

@@ -1,180 +0,0 @@
package protect.card_locker.preferences
import android.app.Activity
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.MenuItem
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import 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
class SettingsActivity : CatimaAppCompatActivity() {
private lateinit var binding: SettingsActivityBinding
private lateinit var fragment: SettingsFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = SettingsActivityBinding.inflate(layoutInflater)
setTitle(R.string.settings)
setContentView(binding.root)
Utils.applyWindowInsets(binding.root)
val toolbar = binding.toolbar
setSupportActionBar(toolbar)
enableToolbarBackButton()
// Display the fragment as the main content.
fragment = SettingsFragment()
supportFragmentManager.beginTransaction()
.replace(R.id.settings_container, fragment)
.commit()
// restore reload main state
if (savedInstanceState != null) {
fragment.mReloadMain = savedInstanceState.getBoolean(RELOAD_MAIN_STATE)
}
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
finishSettingsActivity()
}
})
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(RELOAD_MAIN_STATE, fragment.mReloadMain)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val id = item.itemId
if (id == android.R.id.home) {
finishSettingsActivity()
return true
}
return super.onOptionsItemSelected(item)
}
private fun finishSettingsActivity() {
if (fragment.mReloadMain) {
val intent = Intent()
intent.putExtra(MainActivity.RESTART_ACTIVITY_INTENT, true)
setResult(RESULT_OK, intent)
} else {
setResult(RESULT_OK)
}
finish()
}
class SettingsFragment : PreferenceFragmentCompat() {
var mReloadMain: Boolean = false
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preferences)
// Show pretty names and summaries
val themePreference = findPreference<ListPreference>(getString(R.string.settings_key_theme))
themePreference!!.setOnPreferenceChangeListener { _, o ->
when (o.toString()) {
getString(R.string.settings_key_light_theme) -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
}
getString(R.string.settings_key_dark_theme) -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
}
else -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
}
}
true
}
val oledDarkPreference = findPreference<Preference>(getString(R.string.settings_key_oled_dark))
oledDarkPreference!!.setOnPreferenceChangeListener { _, _ ->
refreshActivity(true)
true
}
val localePreference =
findPreference<ListPreference>(getString(R.string.settings_key_locale))!!
localePreference.let {
val entryValues = it.entryValues
val entries = entryValues.map { entry ->
if (entry.isEmpty()) {
getString(R.string.settings_system_locale)
} else {
val entryLocale = Utils.stringToLocale(entry.toString())
entryLocale.getDisplayName(entryLocale)
}
}
it.entries = entries.toTypedArray()
// Make locale picker preference in sync with system settings
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val sysLocale = AppCompatDelegate.getApplicationLocales()[0]
if (sysLocale == null) {
// Corresponds to "System"
it.value = ""
} else {
// Need to set preference's value to one of localePreference.getEntryValues() to match the locale.
// Locale.toLanguageTag() theoretically should be one of the values in localePreference.getEntryValues()...
// But it doesn't work for some locales. so trying something more heavyweight.
// Obtain all locales supported by the app.
val appLocales = entryValues.map { entry -> Utils.stringToLocale(entry.toString()) }
// Get the app locale that best matches the system one
val bestMatchLocale = Utils.getBestMatchLocale(appLocales, sysLocale)
// Get its index in supported locales
val index = appLocales.indexOf(bestMatchLocale)
// Set preference value to entry value at that index
it.value = entryValues[index].toString()
}
}
}
localePreference.setOnPreferenceChangeListener { _, newValue ->
// See corresponding comment in Utils.updateBaseContextLocale for Android 6- notes
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
refreshActivity(true)
return@setOnPreferenceChangeListener true
}
val newLocale = newValue as String
// If newLocale is empty, that means "System" was selected
AppCompatDelegate.setApplicationLocales(if (newLocale.isEmpty()) LocaleListCompat.getEmptyLocaleList() else LocaleListCompat.create(Utils.stringToLocale(newLocale)))
true
}
// Disable content provider on SDK < 23 since dangerous permissions
// are granted at install-time
val contentProviderReadPreference = findPreference<Preference>(getString(R.string.settings_key_allow_content_provider_read))
contentProviderReadPreference!!.isVisible =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
// Hide crash reporter settings on builds it's not enabled on
val crashReporterPreference = findPreference<Preference>("acra.enable")
crashReporterPreference!!.isVisible = BuildConfig.useAcraCrashReporter
}
private fun refreshActivity(reloadMain: Boolean) {
mReloadMain = reloadMain || mReloadMain
activity?.recreate()
}
}
companion object {
private const val RELOAD_MAIN_STATE = "mReloadMain"
}
}

View File

@@ -0,0 +1,421 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="protect.card_locker.MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
style="?attr/toolbarStyle" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="?attr/actionBarSize"
android:paddingVertical="8dp"
android:clipToPadding="false">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/version_history"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp"
android:paddingHorizontal="16dp"
android:background="?android:selectableItemBackground">
<TextView
android:id="@+id/version_history_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/version_history"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/version_history_sub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/version_history_main" />
<TextView
android:importantForAccessibility="no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:fontFamily="sans-serif-medium"
android:text="@string/arrow"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/credits"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp"
android:paddingHorizontal="16dp"
android:background="?android:selectableItemBackground">
<TextView
android:id="@+id/credits_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/credits"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/credits_sub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/credits_main" />
<TextView
android:importantForAccessibility="no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:fontFamily="sans-serif-medium"
android:text="@string/arrow"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/translate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp"
android:paddingHorizontal="16dp"
android:background="?android:selectableItemBackground">
<TextView
android:id="@+id/translate_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/help_translate_this_app"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/translate_sub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/translate_platform"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/translate_main"/>
<TextView
android:importantForAccessibility="no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:fontFamily="sans-serif-medium"
android:text="@string/arrow"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/license"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp"
android:paddingHorizontal="16dp"
android:background="?android:selectableItemBackground">
<TextView
android:id="@+id/license_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/license"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/license_sub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/app_license"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/license_main"/>
<TextView
android:importantForAccessibility="no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:fontFamily="sans-serif-medium"
android:text="@string/arrow"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/repo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp"
android:paddingHorizontal="16dp"
android:background="?android:selectableItemBackground">
<TextView
android:id="@+id/repo_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/source_repository"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/repo_sub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/on_github"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/repo_main" />
<TextView
android:importantForAccessibility="no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:fontFamily="sans-serif-medium"
android:text="@string/arrow"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/privacy"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp"
android:paddingHorizontal="16dp"
android:background="?android:selectableItemBackground">
<TextView
android:id="@+id/privacy_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/privacy_policy"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/privacy_sub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/and_data_usage"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/privacy_main" />
<TextView
android:importantForAccessibility="no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:fontFamily="sans-serif-medium"
android:text="@string/arrow"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/donate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp"
android:paddingHorizontal="16dp"
android:background="?android:selectableItemBackground">
<TextView
android:id="@+id/donate_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/donate"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:importantForAccessibility="no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:fontFamily="sans-serif-medium"
android:text="@string/arrow"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/rate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp"
android:paddingHorizontal="16dp"
android:background="?android:selectableItemBackground">
<TextView
android:id="@+id/rate_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/rate_this_app"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/rate_sub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/on_google_play"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/rate_main" />
<TextView
android:importantForAccessibility="no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:fontFamily="sans-serif-medium"
android:text="@string/arrow"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/report_error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp"
android:paddingHorizontal="16dp"
android:background="?android:selectableItemBackground">
<TextView
android:id="@+id/report_error_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/report_error"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/report_error_sub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/report_error_main"
app:layout_constraintStart_toStartOf="parent"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:textSize="16sp"
android:text="@string/on_github" />
<TextView
android:importantForAccessibility="no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:fontFamily="sans-serif-medium"
android:text="@string/arrow"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -191,32 +191,6 @@
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<!-- Barcode encoding -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/inputPadding"
android:paddingTop="@dimen/inputPadding"
android:orientation="horizontal">
<!-- Barcode type -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/barcodeEncodingView"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:hint="@string/barcodeEncoding"
android:labelFor="@+id/barcodeEncodingField">
<AutoCompleteTextView
android:id="@+id/barcodeEncodingField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<!-- Barcode -->
<LinearLayout android:orientation="horizontal"
android:layout_marginTop="10.0dp"
@@ -302,24 +276,6 @@
android:paddingTop="@dimen/inputPadding"
android:orientation="horizontal">
<!-- Currency -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/balanceCurrencyView"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:hint="@string/currency"
android:labelFor="@+id/balanceCurrencyField">
<AutoCompleteTextView
android:id="@+id/balanceCurrencyField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"/>
</com.google.android.material.textfield.TextInputLayout>
<!-- Balance -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/balanceView"
@@ -338,6 +294,24 @@
android:digits="0123456789,." />
</com.google.android.material.textfield.TextInputLayout>
<!-- Currency -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/balanceCurrencyView"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:hint="@string/currency"
android:labelFor="@+id/balanceCurrencyField">
<AutoCompleteTextView
android:id="@+id/balanceCurrencyField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<!-- Valid from -->

View File

@@ -40,16 +40,10 @@
android:titleCondensed="@string/unarchive"
app:showAsAction="never"/>
<item
android:id="@+id/action_duplicate"
android:title="@string/duplicateCard"
android:titleCondensed="@string/duplicateCard"
app:showAsAction="never" />
<item
android:id="@+id/action_delete"
android:icon="@drawable/ic_delete_white_24dp"
android:title="@string/delete"
android:titleCondensed="@string/delete"
app:showAsAction="never"/>
</menu>
</menu>

View File

@@ -21,25 +21,25 @@
app:showAsAction="always">
<menu>
<item
android:id="@+id/action_archive"
android:title="@string/archive"
app:showAsAction="never"/>
<item
android:id="@+id/action_unarchive"
android:title="@string/unarchive"
app:showAsAction="never"/>
<item
android:id="@+id/action_duplicate"
android:title="@string/duplicateCard"
app:showAsAction="never" />
<item
android:id="@+id/action_archive"
android:title="@string/archive"
app:showAsAction="never"/>
<item
android:id="@+id/action_unarchive"
android:title="@string/unarchive"
app:showAsAction="never"/>
<item
android:id="@+id/action_delete"
android:title="@string/delete"
app:showAsAction="never"/>
</menu>
</item>

View File

@@ -7,93 +7,80 @@ Heimen Stoffels
Oğuz Ersen
FC (Fay) Stegerman
StoyanDimitrov
大王叫我来巡山
B o d o
SlavekB
Katharine Chui
SlavekB
mondstern
IllusiveMan196
Silvério Santos
大王叫我来巡山
Altonss
Edgars Andersons
Joel A
B o d o
Michael Moroni
Liner Seven
Priit Jõerüüt
Eric
Joel A
Silvério Santos
Максим Горпиніч
GitSpoon
GM
Fjuro
Priit Jõerüüt
laralem
Petr Novák
Edgars Andersons
Taco
nadiafekihahmed
pfaffenrodt
Aayush Gupta
Scrambled777
josé m
ikanakova
Nyatsuki
Giovanni Donisi
Milo Ivir
HudobniVolk
Горпиніч Максим Олександрович
Vasilis
Kachelkaiser
Jiri Grönroos
Warder
Nyatsuki
josé m
Samantaz Fox
Balázs Meskó
Milo Ivir
Fjuro
Cliff Heraldo
Sergio Paredes
Ankit Tiwari
109247019824
Feike Donia
Arno-github
Warder
Kachelkaiser
Jose Delvani
Milan Šalka
Robin
mdvhimself
தமிழ்நேரம்
damjang
Govindgopalyadav
Milan Šalka
GitSpoon
Skrripy
Vasilis
huuhaa
தமிழ் நேரம்
waffshappen
Marnick L'Eau
ngocanhtve
aradxxx
StellarSand
Quentin PAGÈS
Projjal Moitra
e-michalak
Robin
JungHee Lee
hajertabbane
inavleb
Ziad OUALHADJ
Robin Liu
Aliaksandr Trush
Denis Shilin
Traductor
Gideon
Renko
Ricky Tigg
Renko
Denis Shilin
しいたけ
Alexander Ivanov
Miha Frangež
stavpup
mrestivill
ehrt74
Virginie
Tim Trek
MisterCosta96
arshbeerSingh
Augustin LAVILLE
Freddo espresso
vasudev-cell
Kim Seohyun
rudy3
Michael Gangolf
PRATHAMESH BHAGAT
Peter Dave Hello
Michael Gangolf
rudy3
Kim Seohyun
Govind S Nair
Freddo espresso
Augustin LAVILLE
arshbeerSingh
MisterCosta96
Aliaksandr Trush

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">Catima</string>
<string name="action_search">Soek</string>
<string name="action_add">Voeg by</string>
<string name="save">Stoor</string>
<plurals name="selectedCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> geselekteer</item>
<item quantity="other"><xliff:g>%d</xliff:g> geselekteer</item>
</plurals>
</resources>

View File

@@ -2,7 +2,7 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" xmlns:tools="http://schemas.android.com/tools">
<string name="action_search">بحث</string>
<string name="action_add">أضف</string>
<string name="noGiftCards">اضغط على زر الإضافة + لإضافة بطاقة، أو استورد من القائمة خلال ⋮</string>
<string name="noGiftCards">اضغط على زر الإضافة + لإضافة بطاقة، أو استورد من القائمة.</string>
<string name="noMatchingGiftCards">لا نتائج. حاول تغيير كلمات البحث.</string>
<string name="storeName">اسم</string>
<string name="note">مذكرة</string>
@@ -29,7 +29,7 @@
<string name="noCardExistsError">لا يمكن العثور على هذه البطاقة</string>
<string name="failedParsingImportUriError">لا يمكن تحليل الرابط المستورد</string>
<string name="importExport">استيراد/تصدير</string>
<string name="importExportHelp">انشاء نسخة احتياطية من بياناتك يسمح بنقلها إلى جهاز آخر.</string>
<string name="importExportHelp">دعم بياناتك يسمح بنقلها إلى جهاز آخر.</string>
<string name="importFailed">تعذر إجراء الاستيراد</string>
<string name="exportSuccessfulTitle">متصدر</string>
<string name="exportFailedTitle">فشل التصدير</string>
@@ -40,13 +40,16 @@
<string name="app_copyright_old">بناء على Loyalty Card Keychain
\nحقوق النشر © 2016-2020 Branden Archer</string>
<string name="app_license">البرمجيات الحرة متروكة الحقوق, ترخيص +GPLv3</string>
<string name="app_libraries">مكتبات الطرف الثالث : <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_libraries">مكتبات الطرف الثالث الحرة: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="selectBarcodeTitle">اختار الباركود</string>
<string name="thumbnailDescription">صورة مصغرة</string>
<string name="starImage">نجم مفضل</string>
<string name="settings">الإعدادات</string>
<string name="settings_light_theme">فاتحة</string>
<string name="settings_dark_theme">داكنة</string>
<string name="settings_card_orientation">اتجاه الشاشة</string>
<string name="settings_portrait_orientation">الوضع الرأسي</string>
<string name="settings_landscape_orientation">الوضع الأفقي</string>
<string name="settings_theme">مظهر</string>
<string name="settings_display_barcode_max_brightness">شاشة ساطعة</string>
<string name="importSuccessful">تم استيراد البيانات</string>
@@ -60,7 +63,7 @@
<string name="group_updated">تم تحديث المجموعة</string>
<string name="all">الكل</string>
<string name="deleteConfirmationGroup">هل تريد حذف المجموعة؟</string>
<string name="failedOpeningFileManager">فشل فتح مدير الملفات</string>
<string name="failedOpeningFileManager">ثبِّت مدير الملفات أولاً.</string>
<string name="moveUp">تحرك لأعلى</string>
<string name="addFromImage">حدد صورة من المعرض</string>
<string name="balance">الرصيد</string>
@@ -71,9 +74,11 @@
<string name="privacy_policy">سياسة الخصوصية</string>
<string name="accept">قبول</string>
<string name="importCatima">الاستيراد من Catima</string>
<string name="importCatimaMessage">حدّد ملفك تصدير من Catima للاستيراد.\nإنشئها من قائمة الاستيراد / التصدير لتطبيق Catima آخر بالضغط على تصدير .</string>
<string name="importCatimaMessage">حدّد ملفك <i>catima.zip</i> تصدير من Catima للاستيراد. \nإنشئها من قائمة الاستيراد / التصدير لتطبيق Catima آخر بالضغط على تصدير هناك أولاً.</string>
<string name="importFidme">الاستيراد من FidMe</string>
<string name="importFidmeMessage">حدّد ملفك <i>fidme-export-request-xxxxxx.zip</i> تصدير من FidMe للاستيراد، ثم حدد أنواع الباركود يدويًا بعد ذلك. \nإنشئها من ملف تعريف FidMe الخاص بك عن طريق اختيار حماية البيانات ثم الضغط على استخراج بياناتي أولاً.</string>
<string name="importStocardMessage">حدد ملفك <i>***.zip</i> تصدير من Stocard للاستيراد.
\nاحصل عليه عن طريق إرسال بريد إلكتروني إلى support@stocardapp.com لطلب تصدير بياناتك.</string>
<string name="importVoucherVault">الاستيراد من Voucher Vault</string>
<string name="importVoucherVaultMessage">حدّد ملفك <i>vouchervault.json</i> تصدير من Voucher Vault للاستيراد. \nإنشئها بالضغط على تصدير في Voucher Vault أولاً.</string>
<string name="barcodeId">قيمة الباركود</string>
@@ -101,6 +106,14 @@
<string name="settings_locale">لغة</string>
<string name="settings_system_locale">النظام</string>
<string name="setIcon">تعيين الصورة المصغرة</string>
<string name="settings_catima_theme">Catima</string>
<string name="settings_pink_theme">زهري</string>
<string name="settings_magenta_theme">أرجواني</string>
<string name="settings_violet_theme">البنفسجي</string>
<string name="settings_blue_theme">أزرق</string>
<string name="settings_sky_blue_theme">أزرق سماوي</string>
<string name="settings_green_theme">أخضر</string>
<string name="settings_brown_theme">بني</string>
<string name="app_contributors">أصبح ممكنًا بواسطة: <xliff:g id="app_contributors">%s</xliff:g></string>
<string name="sort">فرز</string>
<string name="showMoreInfo">اظهر المعلومات</string>
@@ -170,14 +183,16 @@
<string name="about_title_fmt">حول <xliff:g id="app_name">%s</xliff:g></string>
<string name="debug_version_fmt">نسخة: <xliff:g id="version">%s</xliff:g></string>
<string name="settings_system_theme">النظام</string>
<string name="settings_lock_on_opening_orientation">قفل على الاتجاه عند فتح البطاقة</string>
<string name="app_resources">موارد الطرف الثالث الحرة: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="settings_follow_system_orientation">نظام المتابعة</string>
<string name="groups">مجموعات</string>
<string name="settings_keep_screen_on">حافظ على الشاشة قيد التشغيل</string>
<string name="intent_import_card_from_url_share_text">اريد مشاركة بطاقة معك</string>
<string name="groupsList">مجموعات: <xliff:g>%s</xliff:g></string>
<string name="settings_disable_lockscreen_while_viewing_card">منع قفل الشاشة</string>
<string name="leaveWithoutSaveTitle">خروج</string>
<string name="editGroup">تعديل المجموعة: <xliff:g>%s</xliff:g></string>
<string name="editGroup">تعديل المجموعه: <xliff:g>%s</xliff:g></string>
<plurals name="groupCardCount">
<item quantity="zero"><xliff:g>%d</xliff:g> بطاقة</item>
<item quantity="one"><xliff:g>%d</xliff:g> بطاقة</item>
@@ -214,9 +229,11 @@
<string name="sort_by_expiry">انقضاء</string>
<string name="importLoyaltyCardKeychain">الاستيراد من Loyalty Card Keychain</string>
<string name="importLoyaltyCardKeychainMessage">حدّد ملفك <i>LoyaltyCardKeychain.csv</i> التصدير من Loyalty Card Keychain للاستيراد. \nإنشئها من قائمة الاستيراد / التصدير في Loyalty Card Keychain بالضغط على تصدير هناك أولاً.</string>
<string name="failedGeneratingShareURL">تعذر إنشاء عنوان URL قابل للمشاركة</string>
<string name="importStocard">الاستيراد من Stocard</string>
<string name="failedGeneratingShareURL">تعذر إنشاء عنوان URL قابل للمشاركة. الرجاء الإبلاغ عن هذا.</string>
<string name="help_translate_this_app">ساعد في ترجمة هذا التطبيق</string>
<string name="on_google_play">على Google Play</string>
<string name="settings_theme_color">لون المظهر</string>
<string name="previousCard">السابق</string>
<string name="nextCard">التالي</string>
<string name="failedToRetrieveImageFile">فشل في استخراج ملف الصورة</string>
@@ -275,6 +292,7 @@
<string name="addWithoutBarcode">إضافة بدون باركود</string>
<string name="field_must_not_be_empty">يجب ألا يكون الحقل فارغا</string>
<string name="app_name">كاتيما</string>
<string name="settings_follow_sensor_orientation">التدوير دائمًا ( تجاهل إعدادات النظام)</string>
<string name="add_manually_warning_title">الفحص موصى به</string>
<string name="continue_">استمر</string>
<string name="spend">انفق</string>
@@ -293,7 +311,7 @@
<string name="useBackImage">استخدم صورة خلفية</string>
<string name="addFromPkpass">اختر ملف الدفتر (.pkpass)</string>
<string name="unsupportedFile">هذا الملف غير مدعوم</string>
<string name="generic_error_please_retry">حدث خطأ ما</string>
<string name="generic_error_please_retry">نعتذر، حدث خطأ ما، حاول مرة أخرى...</string>
<string name="settings_use_volume_keys_navigation">بدّل البطاقات باستخدام أزرار الصوت</string>
<string name="settings_use_volume_keys_navigation_summary">بدّل البطاقات الظاهرة باستخدام أزرار الصوت</string>
<string name="settings_category_title_cards_overview">نظرة عامة على البطاقات</string>
@@ -310,7 +328,4 @@
<string name="sort_by_valid_from">صالح من</string>
<string name="width">العرض</string>
<string name="setBarcodeWidth">تعيين عرض الرمز الشريطي \"باركود\"</string>
<string name="card_list_widget_name">قائمة البطاقات</string>
<string name="cardWithNumber">البطاقة <xliff:g>%d</xliff:g></string>
<string name="cardWithNumberAndLocale">البطاقة <xliff:g>%d</xliff:g> (<xliff:g>%s</xliff:g>)</string>
</resources>

View File

@@ -84,6 +84,11 @@
<string name="settings_system_theme">Сістэмная</string>
<string name="settings_light_theme">Светлая</string>
<string name="settings_dark_theme">Цёмная</string>
<string name="settings_card_orientation">Арыентацыя экрана</string>
<string name="settings_follow_sensor_orientation">Заўсёды паварочваць (ігнаруе налады сістэмы)</string>
<string name="settings_portrait_orientation">Партрэтная</string>
<string name="settings_landscape_orientation">Альбомная</string>
<string name="settings_lock_on_opening_orientation">Зафіксаваць арыентацыю, якая выкарыстоўваецца пры адкрыцці карты</string>
<string name="settings_keep_screen_on_summary">Адключае тайм-аўт экрана падчас прагляду карты</string>
<string name="settings_disable_lockscreen_while_viewing_card">Прадухіляць блакіроўку экрана</string>
<string name="settings_disable_lockscreen_while_viewing_card_summary">Адключае блакіроўку экрана падчас прагляду карты</string>
@@ -134,6 +139,7 @@
<string name="importCatima">Імпарт з Catima</string>
<string name="importFidme">Імпарт з FidMe</string>
<string name="importLoyaltyCardKeychain">Імпарт з Loyalty Card Keychain</string>
<string name="importStocard">Імпарт з Stocard</string>
<string name="importVoucherVault">Імпарт з Voucher Vault</string>
<string name="barcodeId">Значэнне штрыхкода</string>
<string name="importVoucherVaultMessage">Каб імпартаваць, выберыце файл <i>vouchervault.json</i> з Voucher Vault. \nСтварыце яго, націснуўшы Экспарт у Voucher Vault .</string>
@@ -158,6 +164,9 @@
<string name="settings_oled_dark">Чысты чорны фон для цёмнай тэмы</string>
<string name="selectColor">Выбраць колер</string>
<string name="setIcon">Задаць мініяцюру</string>
<string name="settings_theme_color">Колер тэмы</string>
<string name="settings_catima_theme">Catima</string>
<string name="settings_magenta_theme">Пурпурны</string>
<string name="app_contributors">Стала магчымым дзякуючы: <xliff:g id="app_contributors">%s</xliff:g></string>
<string name="sort">Сартаваць</string>
<string name="showMoreInfo">Паказаць інфармацыю</string>
@@ -259,6 +268,7 @@
<string name="addFromImage">Выбраць малюнак з галерэі</string>
<string name="settings_keep_screen_on">Трымаць экран уключаным</string>
<string name="app_resources">Бясплатныя староннія рэсурсы: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="settings_follow_system_orientation">Як у сістэме</string>
<string name="leaveWithoutSaveTitle">Выйсці</string>
<string name="settings_allow_content_provider_read_title">Дазволіць іншым праграмам доступ да маіх даных</string>
<string name="settings_display_barcode_max_brightness">Павялічваць яркасць экрану</string>
@@ -267,11 +277,14 @@
<string name="editBarcode">Рэдагаваць штрыхкод</string>
<string name="leaveWithoutSaveConfirmation">Выйсці без захавання?</string>
<string name="importLoyaltyCardKeychainMessage">Каб імпартаваць, выберыце файл <i>LoyaltyCardKeychain.csv</i> з Loyalty Card Keychain. \nСтварыце яго з меню «Імпарт/Экспарт» у Loyalty Card Keychain, спачатку націснуўшы там «Экспарт».</string>
<string name="importStocardMessage">Каб імпартаваць, выберыце файл <i>***.zip</i> з Stocard. \nАтрымайце яго па электроннай пошце support@stocardapp.com з запытам на экспарт вашых даных.</string>
<string name="frontImageDescription">Пярэдні відарыс</string>
<string name="groupsList">Групы: <xliff:g>%s</xliff:g></string>
<string name="switchToBackImage">Пераключыцца на задні відарыс</string>
<string name="importFidmeMessage">Каб імпартаваць, выберыце файл <i>fidme-export-request-xxxxxx.zip</i> з FidMe, а потым уручную выберыце тыпы штрыхкодаў. \nСтварыце яго з вашага профілю FidMe, выбраўшы \"Абарону даных\", а затым націснуўшы \"Выняць мае даныя\".</string>
<string name="importCatimaMessage">Каб імпартаваць, выберыце файл <i>catima.zip</i> з Catima. \nСтварыце яго з меню \"Імпарт/Экспарт\" іншай праграмы Catima, спачатку націснуўшы там \"Экспарт\".</string>
<string name="settings_sky_blue_theme">Нябесна-блакітны</string>
<string name="settings_brown_theme">Карычневы</string>
<string name="switchToBarcode">Пераключыцца на штрыхкод</string>
<string name="settings_locale">Мова</string>
<plurals name="groupCardCountWithArchived">
@@ -283,8 +296,12 @@
<string name="unarchived">Карта разархівавана</string>
<string name="updateBarcodeQuestionText">Вы змянілі ID. Вы хочаце таксама абнавіць штрыхкод, каб выкарыстоўваць тое ж значэнне?</string>
<string name="no">Не</string>
<string name="settings_pink_theme">Ружовы</string>
<string name="settings_system_locale">Сістэма</string>
<string name="settings_violet_theme">Фіялетавы</string>
<string name="multipleBarcodesFoundPleaseChooseOne">Які са знойдзеных штрыхкодаў вы хочаце выкарыстоўваць?</string>
<string name="settings_blue_theme">Сіні</string>
<string name="settings_green_theme">Зялёны</string>
<string name="report_error">Паведаміць пра памылку</string>
<string name="failedLaunchingPhotoPicker">Не атрымалася знайсці праграму для галерэі, якая падтрымліваецца</string>
<string name="unsupportedFile">Гэты файл не падтрымліваецца</string>
@@ -297,14 +314,4 @@
<string name="generic_error_please_retry">На жаль, нешта пайшло не так, паспрабуйце яшчэ раз...</string>
<string name="setBarcodeWidth">Задаць шырыню штрыхкода</string>
<string name="app_license">Свабоднае копілефт праграмнае забеспячэнне, ліцэнзаванае паводле GPLv3+</string>
<string name="cardWithNumber">Карта <xliff:g>%d</xliff:g></string>
<string name="cardWithNumberAndLocale">Карта <xliff:g>%d</xliff:g> (<xliff:g>%s</xliff:g>)</string>
<string name="pleaseDoNotRotateTheDevice">Калі ласка, не паварочвайце прыладу, бо гэта адменіць дзеянне</string>
<string name="acra_explain_crash">Калі магчыма, дадайце больш падрабязную інфармацыю пра тое, што вы тут рабілі:</string>
<string name="acra_crash_email_subject">Справаздача аб збоі <xliff:g id="app_name">%s</xliff:g></string>
<string name="pref_enable_acra">Запытваць дазвол на адпраўку справаздач аб збоях</string>
<string name="pref_enable_acra_summary">Калі гэта ўключана, вам будзе прапанавана паведаміць пра збой, калі ён адбудзецца. Справаздачы аб збоях ніколі не адпраўляюцца аўтаматычна.</string>
<string name="card_list_widget_name">Спіс карт</string>
<string name="card_list_widget_empty">Пасля таго, як вы дадасце некалькі картак лаяльнасці ў Catima, яны з\'явяцца тут. Калі ў вас ёсць карты, пераканайцеся, што яны не ўсе заархіваваны.</string>
<string name="acra_catima_has_crashed">Прабачце, але ў праграме <xliff:g id="app_name">%s</xliff:g> адбыўся збой. Калі ласка, дапамажыце нам выправіць гэту праблему, даслаўшы нам справаздачу аб памылцы.</string>
</resources>

View File

@@ -16,13 +16,13 @@
<string name="note">Бележка</string>
<string name="storeName">Наименование</string>
<string name="noMatchingGiftCards">Няма резултати. Променете критериите за търсене.</string>
<string name="noGiftCards">Докоснете бутона +, за да добавите карта или внесете от менюто ⋮</string>
<string name="noGiftCards">Докоснете бутона +, за да добавите карта или внесете от менюто ⋮.</string>
<string name="all">Всички</string>
<plurals name="groupCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> карта</item>
<item quantity="other"><xliff:g>%d</xliff:g> карти</item>
</plurals>
<string name="failedOpeningFileManager">Грешка при отваряне управление на файлове</string>
<string name="failedOpeningFileManager">Инсталирайте приложение за управление на файлове.</string>
<string name="app_license">Свободен софтуер с авторски права, лицензиран под GPLv3+</string>
<string name="frontImageDescription">Снимка на предната страна</string>
<string name="backImageDescription">Снимка на задната страна</string>
@@ -45,9 +45,10 @@
<string name="sameAsCardId">Като номера</string>
<string name="barcodeId">Стойност на щрихкода</string>
<string name="importLoyaltyCardKeychain">Внасяне от Loyalty Card Keychain</string>
<string name="importFidmeMessage">Изберете предварително изнесен файл от FidMe, който да внесете и ръчно изберете вида на щрихкодовете.\nСъздайте такъв файл от Data Protection в менюто на профила във FidMe и изберете „Extract my data“.</string>
<string name="importFidmeMessage">Изберете файла <i>fidme-export-request-xxxxxx.zip</i>, предварително изнесен от FidMe и ръчно изберете вида на щрихкодовете.
\nСъздайте такъв файл от Data Protection в менюто на профила във FidMe и изберете „Extract my data“.</string>
<string name="importFidme">Внасяне от FidMe</string>
<string name="exportOptionExplanation">Данните ще бъдат запазени на място по ваш избор</string>
<string name="exportOptionExplanation">Данните ще бъдат запазени на място по ваш избор.</string>
<string name="accept">Приемане</string>
<string name="privacy_policy">Политика за личните данни</string>
<string name="app_loyalty_card_keychain">Loyalty Card Keychain</string>
@@ -69,7 +70,7 @@
<string name="expiryStateSentence">Валидност до: <xliff:g>%s</xliff:g></string>
<string name="expiryStateSentenceExpired">Изтекла: <xliff:g>%s</xliff:g></string>
<string name="balanceSentence">Наличност: <xliff:g>%s</xliff:g></string>
<string name="noGroups">Докоснете бутона +, за да добавите списък</string>
<string name="noGroups">Докоснете бутона +, за да добавите списък.</string>
<string name="groups">Списъци</string>
<string name="enter_group_name">Въведете име на списъка</string>
<string name="intent_import_card_from_url_share_text">Искам да споделя тази карта с вас</string>
@@ -92,21 +93,22 @@
<string name="importFailedTitle">Грешка при внасяне</string>
<string name="exportSuccessfulTitle">Резултат от изнасяне</string>
<string name="importSuccessfulTitle">Резултат от внасяне</string>
<string name="importExportHelp">Резервните копия на данните дават възможност да ги премествате на друго устройство</string>
<string name="importExportHelp">Резервните копия на данните ви дават възможност да ги премествате на друго устройство.</string>
<string name="exportName">Изнасяне</string>
<string name="importExport">Внасяне/изнасяне</string>
<string name="sendLabel">Изпращане…</string>
<string name="scanCardBarcode">Снемане на щрихкод</string>
<string name="editCardTitle">Променяне на карта</string>
<string name="editCardTitle">Редактиране на карта</string>
<string name="share">Споделя</string>
<string name="ok">Добре</string>
<string name="importSuccessful">Данните са внесени</string>
<string name="chooseImportType">Внасяне на данни на</string>
<string name="importCatimaMessage">Изберете предварително изнесен файл от Catima, който да внесете.\nСъздайте такъв файл от меню Внасяне/изнасяне от друго устройство с Catima като изберете Изнасяне.</string>
<string name="importCatimaMessage">Изберете файла <i>catima.zip</i>, предварително изнесен от Catima.
\nСъздайте такъв файл от меню Внасяне/изнасяне от друго устройство с Catima като изберете Изнасяне.</string>
<string name="importOptionFilesystemButton">Избиране от файлова система</string>
<string name="importOptionFilesystemExplanation">Изберете определен файл от файловата система</string>
<string name="app_resources">Ресурси: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Библиотеки: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="importOptionFilesystemExplanation">Изберете определен файл от файловата система.</string>
<string name="app_resources">Свободни ресурси: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Свободни библиотеки: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="debug_version_fmt">Издание: <xliff:g id="version">%s</xliff:g></string>
<string name="about_title_fmt">Относно <xliff:g id="app_name">%s</xliff:g></string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Всички права запазени © 2019<xliff:g>%d</xliff:g> Силвия ван Ос и сътрудници</string>
@@ -127,11 +129,16 @@
<string name="addManually">Ръчно въвеждане</string>
<string name="leaveWithoutSaveConfirmation">Оставяте промените незапазени\?</string>
<string name="unsupportedBarcodeType">Щрихкод от този вид не може да бъде показан. Може да бъде поддържан в следващо издание.</string>
<string name="importStocard">Внасяне от Stocard</string>
<string name="importVoucherVault">Внасяне от Voucher Vault</string>
<string name="importVoucherVaultMessage">Изберете предварително изнесен файл от Voucher Vault, който да внесете.\nСъздайте такъв файл от меню „Export“ във Voucher Vault.</string>
<string name="importLoyaltyCardKeychainMessage">Изберете предварително изнесен файл от Loyalty Card Keychain, който да внесете.\nСъздайте такъв файл от меню Внасяне/изнасяне от друго устройство с Loyalty Card Keychain като изберете Изнасяне.</string>
<string name="failedParsingImportUriError">Адресът за внасяне не може да бъде анализиран</string>
<string name="failedGeneratingShareURL">Грешка при създаване на адрес, който да споделите</string>
<string name="importVoucherVaultMessage">Изберете файла <i>vouchervault.json</i>, предварително изнесен от Voucher Vault.
\nСъздайте такъв файл от меню „Export“ във Voucher Vault.</string>
<string name="importStocardMessage">Изберете файла <i>***.zip</i>, предварително изнесен от Stocard.
\nПолучете го като изпратите писмо на support@stocardapp.com с искане за изнасяне вашите данни.</string>
<string name="importLoyaltyCardKeychainMessage">Изберете файла <i>LoyaltyCardKeychain.csv</i>, предварително изнесен от Loyalty Card Keychain.
\nСъздайте такъв файл от меню Внасяне/изнасяне от друго устройство с Loyalty Card Keychain като изберете Изнасяне.</string>
<string name="failedParsingImportUriError">Препратката не може да бъде анализирана за внасяне</string>
<string name="failedGeneratingShareURL">Не може да бъде генериран адрес за споделяне. Изпратете доклад за дефект.</string>
<string name="deleteTitle">Премахване на карта</string>
<plurals name="deleteCardsTitle">
<item quantity="one">Изтриване на <xliff:g>%d</xliff:g> карта</item>
@@ -143,6 +150,15 @@
<item quantity="other">Желаете ли тези <xliff:g>%d</xliff:g> карти да бъдат премахнати\?</item>
</plurals>
<string name="app_contributors">Осъществено от: <xliff:g id="app_contributors">%s</xliff:g></string>
<string name="settings_brown_theme">Кафяво</string>
<string name="settings_green_theme">Зелено</string>
<string name="settings_sky_blue_theme">Небесносиньо</string>
<string name="settings_blue_theme">Синьо</string>
<string name="settings_violet_theme">Виолетово</string>
<string name="settings_magenta_theme">Цикламено</string>
<string name="settings_pink_theme">Розово</string>
<string name="settings_catima_theme">Catima</string>
<string name="settings_theme_color">Цвят на темата</string>
<string name="settings_system_locale">Система</string>
<string name="settings_locale">Език</string>
<string name="noGroupCards">Групата е празна</string>
@@ -172,7 +188,7 @@
<string name="selectColor">Избиране на цвят</string>
<string name="group_name_is_empty">Името на списъка не трябва да е празно</string>
<string name="group_edit">Редактиране на списък</string>
<string name="noGiftCardsGroup">Създайте карти и ги зачислете към списък от тук</string>
<string name="noGiftCardsGroup">Създайте карти и ги зачислите към списък от тук.</string>
<string name="translate_platform">в Weblate</string>
<string name="shortcutSelectCard">Избор на карта</string>
<string name="starred">Със звезда</string>
@@ -184,12 +200,17 @@
</plurals>
<string name="settings_oled_dark">Черен фон за тъмната тема</string>
<string name="include_if_asking_support">Ако искате да потърсите поддръжка, включете следната информация:</string>
<string name="settings_card_orientation">Завъртане на екрана</string>
<string name="settings_follow_system_orientation">Според системата</string>
<string name="settings_portrait_orientation">Портрет</string>
<string name="settings_landscape_orientation">Пейзаж</string>
<string name="settings_lock_on_opening_orientation">Като при отваряне на картата</string>
<string name="duplicateCard">Дублиране</string>
<string name="archive">Архивиране</string>
<string name="unarchive">Изваждане от архива</string>
<string name="archived">Картата е архивирана</string>
<string name="unarchived">Карта е извадена от архива</string>
<string name="failedLaunchingPhotoPicker">Не е намерено поддържано приложение за избор на изображение</string>
<string name="failedLaunchingPhotoPicker">Не е намерено поддържано приложение за галерия</string>
<plurals name="groupCardCountWithArchived">
<item quantity="one"><xliff:g>%1$d</xliff:g> карта (<xliff:g id="archivedCount">%2$d</xliff:g> архивирана)</item>
<item quantity="other"><xliff:g>%1$d</xliff:g> карти (<xliff:g id="archivedCount">%2$d</xliff:g> архивирани)</item>
@@ -218,8 +239,8 @@
<string name="switchToFrontImage">Показване на предната страна</string>
<string name="switchToBackImage">Показване на задната страна</string>
<string name="switchToBarcode">Показване на щрихкода</string>
<string name="openFrontImageInGalleryApp">Отваряне на изображението на предната страна в приложение за преглед за изображения</string>
<string name="openBackImageInGalleryApp">Отваряне на изображението на задната страна в приложение за преглед за изображения</string>
<string name="openFrontImageInGalleryApp">Отваряне на изображението на предната страна в приложението галерия</string>
<string name="openBackImageInGalleryApp">Отваряне на изображението на задната страна в приложението галерия</string>
<string name="setBarcodeHeight">Задаване на височина на щрихкода</string>
<string name="donate">Даряване</string>
<string name="icon_header_click_text">Задръжте, за да промените миниатюрата</string>
@@ -251,9 +272,10 @@
<string name="addWithoutBarcode">Добавяне на карта без щрихкод</string>
<string name="field_must_not_be_empty">Полето не трябва да е празно</string>
<string name="app_name">Catima</string>
<string name="settings_follow_sensor_orientation">Винаги да се завърта (пренебрегва системната настройка)</string>
<string name="continue_">Продължаване</string>
<string name="add_manually_warning_title">Препоръчително е да сканирате</string>
<string name="add_manually_warning_message">Стойностите от щрихкода и отбелязаните на картата числа в някои случаи се различават. По тази причина при ръчно въвеждане картата може да не работи. Препоръчително е да сканирате щрихкода с камерата. Желаете ли да продължите въпреки това?</string>
<string name="add_manually_warning_message">Стойностите от щрихкода и отбелязаните на картата числа в някои случаи се различават. По тази причина е при ръчно въвеждане картата може да не работи. Силно препоръчително е да сканирате щрихкода с камерата. Желаете ли да продължите въпреки това?</string>
<string name="amountParsingFailed">Неприемлива сума</string>
<string name="spend">Похарчено</string>
<string name="receive">Получено</string>
@@ -280,23 +302,12 @@
<string name="settings_column_count_landscape">Колони в пейзажен изглед</string>
<string name="settings_column_count_portrait">Колони в портретен изглед</string>
<string name="settings_category_title_cards_overview">Списък с карти</string>
<string name="generic_error_please_retry">Възникна грешка</string>
<string name="addFromPkpass">Изберете файл на Passbook (.pkpass / pkpasses)</string>
<string name="generic_error_please_retry">Съжаляваме, нещо се обърка, опитайте отново…</string>
<string name="addFromPkpass">Изберете файл на Passbook (.pkpass)</string>
<string name="unsupportedFile">Този вид файлове не се поддържат</string>
<string name="sort_by_valid_from">Начало валидност</string>
<string name="width">Ширина</string>
<string name="setBarcodeWidth">Задаване ширина на щрихкода</string>
<string name="setBarcodeWidth">Задаване ширина на щрих кода</string>
<string name="card_list_widget_name">Списък с карти</string>
<string name="card_list_widget_empty">Когато добавите карти в Catima те ще се покажат тук. Ако имате карти уверете се, че са извън архива.</string>
<string name="cardWithNumber">Карта <xliff:g>%d</xliff:g></string>
<string name="cardWithNumberAndLocale">Карта <xliff:g>%d</xliff:g> (<xliff:g>%s</xliff:g>)</string>
<string name="pleaseDoNotRotateTheDevice">Не завъртайте устройството, защото това ще прекъсне действието</string>
<string name="acra_catima_has_crashed">За съжаление <xliff:g id="app_name">%s</xliff:g> се срина. Помогнете ни да оправим проблема като ни изпратите доклад за грешката.</string>
<string name="acra_crash_email_subject">Доклад за срив на <xliff:g id="app_name">%s</xliff:g></string>
<string name="pref_enable_acra">Питане преди изпращане на доклад за срив</string>
<string name="pref_enable_acra_summary">Когато е отметнато, при срив ще ви бъде предложено да докладвате за него. Докладите никога не се изпращат автоматично.</string>
<string name="acra_explain_crash">Ако е възможно добавете подробности за вашите действия:</string>
<string name="copy_value">Копиране на стойността</string>
<string name="copied_to_clipboard">Копирано</string>
<string name="nothing_to_copy">Няма стойност</string>
</resources>

View File

@@ -26,6 +26,7 @@
<string name="starImage">তারা ছবি</string>
<string name="importCatima">ক্যাতিনা আগম</string>
<string name="importLoyaltyCardKeychain">আমদানি লয়্যালটি কার্ড কীচেন</string>
<string name="importStocard">স্টো কার্ড আমদানি করুন</string>
<string name="importVoucherVault">আমদানি ভাউচার ভল্ট</string>
<string name="barcodeId">বারকোড আইডি</string>
<string name="sameAsCardId">আইডি আর এটা এক</string>
@@ -50,6 +51,15 @@
<string name="turn_flashlight_off">টর্চলাইট বন্ধ করুন</string>
<string name="settings_locale">লোকেল</string>
<string name="settings_system_locale">সিস্টেম লোকেল</string>
<string name="settings_theme_color">থিম রঙ</string>
<string name="settings_catima_theme">কটিমা থিম</string>
<string name="settings_pink_theme">গোলাপী থিম</string>
<string name="settings_magenta_theme">ম্যাজেন্টা থিম</string>
<string name="settings_violet_theme">ভায়োলেট থিম</string>
<string name="settings_blue_theme">নীল থিম</string>
<string name="settings_sky_blue_theme">আকাশী নীল থিম</string>
<string name="settings_green_theme">সবুজ থিম</string>
<string name="settings_brown_theme">বাদামী থিম</string>
<string name="sort">সাজান</string>
<string name="sort_by_name">নামের দ্বারা সাজান</string>
<string name="sort_by_most_recently_used">সর্বাধিক সম্প্রতি ব্যবহৃত দ্বারা সাজান</string>
@@ -101,6 +111,9 @@
<string name="about_title_fmt"><xliff:g id="app_name">%s</xliff:g>টির সম্পর্কে</string>
<string name="app_resources">মুক্ত সম্পদ যেগুলি আমার নয়: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="thumbnailDescription">থাম্বনেইল</string>
<string name="settings_card_orientation">বারকোড অভিমুখ</string>
<string name="settings_follow_system_orientation">সিস্টেমের অনুসারে</string>
<string name="settings_portrait_orientation">প্রতিকৃতি</string>
<string name="barcodeImageDescriptionWithType">ছবি <xliff:g>%s</xliff:g> বারকোড</string>
<string name="exportName">রপ্তানি</string>
<string name="failedParsingImportUriError">আমদানির URI-টি বোঝা যাচ্ছে না</string>
@@ -126,6 +139,8 @@
<string name="selectBarcodeTitle">বারকোড নির্বাচন করুন</string>
<string name="settings">সেটিংস</string>
<string name="settings_dark_theme">অন্ধকার</string>
<string name="settings_landscape_orientation">অনুভূমিক</string>
<string name="settings_lock_on_opening_orientation">কার্ড খোলার সময় যে অভিমুখ থাকে সেটিতে লক করে দেবেন</string>
<string name="group_name_already_in_use">গ্রুপটির নাম আগে একবার ব্যবহার করে ফেলেছেন</string>
<string name="group_edit">গ্রুপ সম্পাদনা করুন</string>
<string name="group_updated">গ্রুপটি আপডেট করা হল</string>
@@ -188,6 +203,8 @@
\nআপনার FidMe প্রোফাইল থেকে ডেটা সুরক্ষা নির্বাচন করে এবং তারপর প্রথমে আমার ডেটা বের করুন টিপে এটি তৈরি করুন।</string>
<string name="importCatimaMessage">ক্যাটিমা থেকে আমদানি করতে আপনার <i>catima.zip</i> রপ্তানি নির্বাচন করুন।
\nঅন্য Catima অ্যাপের আমদানি/রপ্তানি মেনু থেকে প্রথমে সেখানে রপ্তানি টিপে এটি তৈরি করুন।</string>
<string name="importStocardMessage">আমদানি করতে Stocard থেকে আপনার <i>***.zip</i> এক্সপোর্ট নির্বাচন করুন।
\nআপনার ডেটা রপ্তানির জন্য জিজ্ঞাসা করে support@stocardapp.com ই-মেইল করে এটি পান।</string>
<string name="importVoucherVaultMessage">আমদানি করতে ভাউচার ভল্ট থেকে আপনার <i>vouchervault.json</i> এক্সপোর্ট নির্বাচন করুন।
\nপ্রথমে ভাউচার ভল্টে এক্সপোর্ট টিপে এটি তৈরি করুন।</string>
<string name="settings_oled_dark">অন্ধকার থিমের জন্য খাঁটি কালো পটভূমি</string>

View File

@@ -2,6 +2,10 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="settings_locale">ভাষা</string>
<string name="action_search">খুঁজুন</string>
<string name="settings_pink_theme">গুলাপি</string>
<string name="settings_blue_theme">নীল</string>
<string name="settings_green_theme">সবুজ</string>
<string name="settings_brown_theme">বাদামি</string>
<string name="save">সংরক্ষণ</string>
<string name="cardId">কার্ড আইডি</string>
<string name="barcodeType">বারকোডের ধরন</string>
@@ -15,6 +19,7 @@
<string name="all">সকল</string>
<string name="never">কখনো না</string>
<string name="currency">মুদ্রা</string>
<string name="settings_violet_theme">বেগুনি</string>
<string name="no">না</string>
<string name="nextCard">পরবর্তী</string>
<string name="action_add">যুক্ত করুন</string>
@@ -68,5 +73,6 @@
<string name="permissionReadCardsLabel">কাটিমা কার্ডস পড়ুন</string>
<string name="storageReadPermissionRequired">এই কাজটির জন্য ফোনের স্টোরেজ দেখার অনুমতি লাগবে…</string>
<string name="exportFailedTitle">রপ্তানি ব্যর্থ</string>
<string name="settings_card_orientation">বারকোড অভিমুখ (ওরিয়েন্টেশন)</string>
<string name="app_name">ক্যাটিমা</string>
</resources>

View File

@@ -26,6 +26,7 @@
<string name="starImage">Omiljena zvijezda</string>
<string name="importCatima">Uvezi iz Catima</string>
<string name="importLoyaltyCardKeychain">Uvezi iz Loyalty Card Keychain</string>
<string name="importStocard">Uvezi iz Stokarda</string>
<string name="importVoucherVault">Uvezi iz trezora vaučer</string>
<string name="barcodeId">Barcode vrijednost</string>
<string name="sameAsCardId">Isto kao i kartica</string>
@@ -50,6 +51,15 @@
<string name="turn_flashlight_off">Ugasi lampu</string>
<string name="settings_locale">Jezik</string>
<string name="settings_system_locale">Sistem</string>
<string name="settings_theme_color">Boja teme</string>
<string name="settings_catima_theme">Catima</string>
<string name="settings_pink_theme">Ružičasto</string>
<string name="settings_magenta_theme">Ljubičasto</string>
<string name="settings_violet_theme">Ljubičasto</string>
<string name="settings_blue_theme">Plavo</string>
<string name="settings_sky_blue_theme">Nebo plavo</string>
<string name="settings_green_theme">Zeleno</string>
<string name="settings_brown_theme">Braun</string>
<string name="sort">Poništi sortiranje</string>
<string name="sort_by_name">Ime</string>
<string name="sort_by_most_recently_used">Nedavno Korišten</string>

View File

@@ -7,12 +7,12 @@
<string name="delete">Elimina</string>
<string name="confirm">Confirma</string>
<string name="ok">D\'acord</string>
<string name="importExport">Importa/exporta</string>
<string name="importExport">Importa/Exporta</string>
<string name="exportName">Exporta</string>
<string name="action_search">Cerca</string>
<string name="deleteTitle">Elimina la targeta</string>
<string name="welcome">Benvingut a Catima</string>
<string name="noGiftCards">Fes clic al botó + per afegir una targeta, o importa des del menú</string>
<string name="noGiftCards">Cliqueu el botó + més per afegir una targeta, o importeu-ne des del menú.</string>
<string name="photos">Fotos</string>
<string name="app_name">Catima</string>
<string name="moveDown">Baixar abaix</string>
@@ -24,10 +24,10 @@
<string name="on_google_play">al Google Play</string>
<string name="settings_locale">Idioma</string>
<string name="field_must_not_be_empty">El camp no pot estar buit</string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Copyright © 2019<xliff:g>%d</xliff:g> Sylvia van Os i col·laboradors</string>
<string name="app_copyright_short">Copyright © Sylvia van Os i col·laboradors</string>
<string name="app_license">Programari lliure Copyleft, licència GPLv3+</string>
<string name="app_resources">Recursos de tercers: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Copyright © 2019<xliff:g>%d</xliff:g> Sylvia van Os i contribuïdors</string>
<string name="app_copyright_short">Copyright © Sylvia van Os i contribuïdors</string>
<string name="app_license">Software lliure Copyleft, licència GPLv3+</string>
<string name="app_resources">Recursos lliures de tercers: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="thumbnailDescription">Miniatura</string>
<string name="starImage">Estrella de preferides</string>
<string name="settings">Configuració</string>
@@ -35,6 +35,7 @@
<string name="settings_light_theme">Tema clar</string>
<string name="settings_system_theme">Tema de sistema</string>
<string name="settings_dark_theme">Tema Fosc</string>
<string name="settings_card_orientation">Orientació de la pantalla</string>
<string name="settings_allow_content_provider_read_title">Permet altres apps a accedir a les meves dades</string>
<string name="settings_disable_lockscreen_while_viewing_card_summary">Desactiva el bloqueix la pantalla mentre es visualitza la targeta</string>
<string name="settings_allow_content_provider_read_summary">Les aplicacions han de seguir demanant permís per tenir-hi accés</string>
@@ -42,6 +43,10 @@
<string name="leaveWithoutSaveConfirmation">Vols sortir sense grabar?</string>
<string name="passwordRequired">Introdueixi el password</string>
<string name="turn_flashlight_on">Encendre el llum flash</string>
<string name="settings_magenta_theme">Magenta</string>
<string name="settings_violet_theme">Violeta</string>
<string name="settings_blue_theme">Blau</string>
<string name="settings_green_theme">Verd</string>
<string name="translate_platform">a la Pàgina Web</string>
<string name="report_error">Informar un Error</string>
<string name="archived">Targeta arxivada</string>
@@ -51,13 +56,13 @@
<string name="add_manually_warning_title">Recomenem escanejar</string>
<string name="add_manually_warning_message">En algunes targetes el valor imprès en la targeta no correspon amb el codi registrat en el codi de barres. Per això, introduint manualment el codi pot no funcionar en alguns casos. Recomanem sempre que sigui possible escanejar la targeta amb la càmera. Vol igualment continuar la edició manual?</string>
<string name="continue_">Continuar</string>
<string name="exportOptionExplanation">La informació serà escrita al lloc de la seva elecció</string>
<string name="exportOptionExplanation">La informació serà escrita al lloc de la seva elecció.</string>
<string name="importOptionFilesystemTitle">Importar desde el sistema de fitxers</string>
<string name="importOptionFilesystemButton">Desde el sistema de fitxers</string>
<string name="selectBarcodeTitle">Selecciona el codi de barres</string>
<string name="selectBarcodeTitle">Sel•lecciona el Codi de Barres</string>
<string name="importSuccessful">Dades importades correctament</string>
<string name="exportSuccessful">Dades exportades correctament</string>
<string name="failedOpeningFileManager">No s\'ha pogut obrir el gestor de fitxers</string>
<string name="failedOpeningFileManager">Instala un gestor de fitxers.</string>
<string name="showMoreInfo">Mostrar informació</string>
<string name="version_history">Històric de versions</string>
<string name="sort_by">Ordenar per</string>
@@ -68,8 +73,9 @@
<item quantity="many"><xliff:g>%d</xliff:g> seleccionats</item>
<item quantity="other"><xliff:g>%d</xliff:g> seleccionats</item>
</plurals>
<string name="importOptionFilesystemExplanation">Escull un fitxer especific del sistema de fitxers</string>
<string name="importOptionFilesystemExplanation">Escull un fitxer especific del sistema de fitxers.</string>
<string name="no">No</string>
<string name="settings_pink_theme">Rosa</string>
<string name="sort">Ordenar</string>
<string name="failedToRetrieveImageFile">Ha fallat l\'obtenció del fitxer d\'imatge</string>
<string name="barcodeLongPressMessage">Les imatges només es poden obrir desde la app galeria</string>
@@ -91,19 +97,22 @@
</plurals>
<string name="importCancelled">Importació anulada</string>
<string name="exportCancelled">Exportació cancelada</string>
<string name="noGiftCardsGroup">Crea algunes targetes i després asigna-les en al grup aquí</string>
<string name="noMatchingGiftCards">No hi ha resultats; prova de modificar la cerca.</string>
<string name="noGiftCardsGroup">Crea algunes targetes, asigna-les en un grup aquí.</string>
<string name="noMatchingGiftCards">Sense resultats. Prova a canviar la teva cerca.</string>
<string name="storeName">Nom</string>
<string name="note">Nota</string>
<string name="cardId">Id. de la Targeta</string>
<string name="barcodeType">Tipus de codi de barres</string>
<string name="noBarcode">Sense codi de barres</string>
<string name="settings_portrait_orientation">Vertical</string>
<string name="yes">Si</string>
<string name="addFromPdfFile">Seleccioni un PDF</string>
<string name="errorReadingFile">No s\'ha pogut llegir el fitxer</string>
<string name="failedLaunchingFileManager">No s\'ha pogut trobar un gestor de fitxers compatible</string>
<string name="multipleBarcodesFoundPleaseChooseOne">Quin dels següents codis de barres prefereix utilitzar?</string>
<string name="pageWithNumber">Pàgina <xliff:g>%d</xliff:g></string>
<string name="settings_follow_system_orientation">Seguir el sistema</string>
<string name="settings_landscape_orientation">Horitzontal</string>
<string name="intent_import_card_from_url_share_text">Vull compartir una targeta amb tu</string>
<string name="takePhoto">Fer una foto</string>
<string name="help_translate_this_app">Ajuda a traduïr aquesta app</string>
@@ -125,6 +134,7 @@
<string name="barcodeImageDescriptionWithType">Codi de barres <xliff:g>%s</xliff:g></string>
<string name="about_title_fmt">Sobre <xliff:g id="app_name">%s</xliff:g></string>
<string name="debug_version_fmt">Versió: <xliff:g id="version">%s</xliff:g></string>
<string name="settings_follow_sensor_orientation">Sempre rota (ignora la configuració de sistema)</string>
<string name="settings_display_barcode_max_brightness_summary">Alguns escàners ho necesiten</string>
<string name="settings_keep_screen_on">Mantenir la pantalla encesa</string>
<string name="settings_keep_screen_on_summary">Desactiva el bloqueix de la pantalla mentre mostra una targeta</string>
@@ -141,6 +151,7 @@
<string name="settings_oled_dark">Negre pur en el tema fosc</string>
<string name="selectColor">Sel•leccioni el color</string>
<string name="setIcon">Setegi la miniatura</string>
<string name="settings_theme_color">Color del tema</string>
<string name="app_contributors">Fet possible per: <xliff:g id="app_contributors">%s</xliff:g></string>
<string name="updateBalance">Actualitzar el balanç</string>
<string name="sort_by_name">Nom</string>
@@ -160,21 +171,24 @@
<string name="deleteConfirmation">Vols eliminar de forma permanent aquesta targeta?</string>
<string name="share">Compartir</string>
<string name="sendLabel">Enviar…</string>
<string name="editCardTitle">Editar targeta</string>
<string name="addCardTitle">Afegir targeta</string>
<string name="scanCardBarcode">Escanejar codi de barres</string>
<string name="cardShortcut">Drecera a la targeta</string>
<string name="editCardTitle">Editar Targeta</string>
<string name="addCardTitle">Afegir Targeta</string>
<string name="scanCardBarcode">Escanejar Codi de Barres</string>
<string name="cardShortcut">Drecera a la Targeta</string>
<string name="noCardsMessage">Afegeix primer una targeta</string>
<string name="noCardExistsError">No s\'ha pogut trobar aquesta targeta</string>
<string name="failedParsingImportUriError">No s\'ha pogut analitzar l\'URI d\'importació</string>
<string name="failedParsingImportUriError">No s\'ha pogut analitzar la URI d\'importació</string>
<string name="openFrontImageInGalleryApp">Obrir la imatge frontal a l\'app de galeria</string>
<string name="settings_lock_on_opening_orientation">En obrir la targeta, bloquejar la orientació de la pantalla</string>
<string name="settings_use_volume_keys_navigation_summary">Utilitza els botons de volum per canviar la targeta que es mostra</string>
<string name="updateBarcodeQuestionText">Ha canviat el valor ID. Vol actualitzar també el codi de barres per uter utilitzar el mateix valor?</string>
<string name="settings_sky_blue_theme">Blau fluix</string>
<string name="starred">Preferides</string>
<string name="deleteConfirmationGroup">Vols eliminar aquest grup?</string>
<string name="removeImage">Eliminar imatge</string>
<string name="app_libraries">Llibreries de tercers: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_libraries">Llibreries lliures de tercers: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="settings_display_barcode_max_brightness">Màxima iluminació</string>
<string name="settings_brown_theme">Marró</string>
<string name="manually_enter_barcode_instructions">Introdueixi el ID de la targeta manualment i trii un codi de barres que s\'assembli al de la seva targeta.</string>
<string name="rate_this_app">Valora aquesta app</string>
<string name="exportPasswordHint">Introdueixi el password</string>
@@ -219,7 +233,7 @@
<string name="addFromPkpass">Seleccioni el fitxer Passbook (.pkpass)</string>
<string name="unsupportedFile">Aquest fitxer no està soportat</string>
<string name="settings_use_volume_keys_navigation">Canviar les targetes al prèmer els botons de volum</string>
<string name="noGroups">Feu clic al botó + més per aferir grups pre categoritzar</string>
<string name="noGroups">Clica el botó + per afegir grups per categoritzar.</string>
<string name="noGroupCards">Aquest grup està buit</string>
<string name="group_name_already_in_use">Ja existeix un grup amb aquest nom</string>
<string name="group_updated">Grup actualitzat</string>
@@ -228,44 +242,6 @@
<string name="turn_flashlight_off">Apagar el llum Flash</string>
<string name="settings_oled_dark_summary">Redueix l\'ús de la bateria en pantalles OLED</string>
<string name="settings_system_locale">Idioma del sistema</string>
<string name="settings_catima_theme">Catima</string>
<string name="spend">Gastar</string>
<string name="importExportHelp">Fer una còpia de seguretat de les dades permet moure-les a un altre dispositiu</string>
<string name="importSuccessfulTitle">Importat</string>
<string name="importFailedTitle">La importació ha fallat</string>
<string name="importFailed">No s\'ha pogut realitzar la importació</string>
<string name="exportSuccessfulTitle">Exportat</string>
<string name="exportFailedTitle">L\'exportació ha fallat</string>
<string name="exportFailed">No s\'ha pogut realitzar l\'exportació</string>
<string name="importing">Important…</string>
<string name="exporting">Exportant…</string>
<string name="storageReadPermissionRequired">Cal permís per llegir l\'emmagatzematge per a aquesta acció…</string>
<string name="cameraPermissionRequired">Cal permís per accedir a la càmera per a aquesta acció…</string>
<string name="permissionReadCardsLabel">Legeix targetes Catima</string>
<string name="permissionReadCardsDescription">llegeix les teves targetes Catima i tots els seus detalls, incloses notes i imatges</string>
<string name="cameraPermissionDeniedTitle">No s\'ha pogut accedir a la càmera</string>
<string name="noCameraPermissionDirectToSystemSetting">Per escanejar codis de barres, Catima necessitarà accés a la teva càmera. Toca aquí per canviar la configuració dels permisos.</string>
<string name="about">Sobre</string>
<string name="app_copyright_old">Clauer basat en na Loyalty Card Keychain\ncopyright © 20162020 Branden Archer</string>
<string name="addManually">Introduïu el codi de barres manualment</string>
<string name="addFromImage">Seleccioneu una imatge de la galeria</string>
<string name="groupsList">Grups: <xliff:g>%s</xliff:g></string>
<string name="editGroup">Editeu el grup: <xliff:g>%s</xliff:g></string>
<string name="expiryStateSentence">Caduca el: <xliff:g>%s</xliff:g></string>
<string name="expiryStateSentenceExpired">Caducat el: <xliff:g>%s</xliff:g></string>
<plurals name="balancePoints">
<item quantity="one"><xliff:g>%s</xliff:g> punt</item>
<item quantity="many"><xliff:g>%s</xliff:g> punts</item>
<item quantity="other"/>
</plurals>
<string name="balanceSentence">Saldo: <xliff:g>%s</xliff:g></string>
<string name="card">Targeta</string>
<string name="editBarcode">Editeu el codi de barres</string>
<string name="expiryDate">Data de caducitat</string>
<string name="never">Mai</string>
<string name="chooseExpiryDate">Trieu la data de caducitat</string>
<string name="moveBarcodeToTopOfScreen">Moveu el codi de barres a la part superior de la pantalla</string>
<string name="noBarcodeFound">No s\'ha trobat cap codi de barres</string>
<string name="errorReadingImage">No s\'ha pogut llegir la imatge</string>
<string name="balance">Saldo</string>
<string name="app_loyalty_card_keychain">Loyalty Card Keychain</string>
</resources>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" xmlns:tools="http://schemas.android.com/tools">
<string name="action_add">Přidat</string>
<string name="noGiftCards">Klepněte na tlačítko plus (+) pro přidání karty nebo naimportujete karty z nabídky (⋮)</string>
<string name="noGiftCards">Klepněte na tlačítko plus (+) pro přidání karty nebo naimportujete karty z nabídky (⋮).</string>
<string name="storeName">Název</string>
<string name="note">Poznámka</string>
<string name="cardId">ID karty</string>
@@ -12,12 +12,12 @@
<string name="confirm">Potvrdit</string>
<string name="ok">OK</string>
<string name="sendLabel">Odeslat…</string>
<string name="editCardTitle">Upravit kartu</string>
<string name="editCardTitle">Editovat kartu</string>
<string name="addCardTitle">Přidat kartu</string>
<string name="scanCardBarcode">Naskenovat čárový kód</string>
<string name="importExport">Import/export</string>
<string name="importExport">Import/Export</string>
<string name="exportName">Export</string>
<string name="importExportHelp">Zálohování dat vám umožní přesunout je do jiného zařízení</string>
<string name="importExportHelp">Zálohování dat vám umožní přesunout je do jiného zařízení.</string>
<string name="importSuccessfulTitle">Importováno</string>
<string name="importFailedTitle">Import selhal</string>
<string name="importFailed">Import nelze provést</string>
@@ -27,7 +27,7 @@
<string name="importing">Importuji…</string>
<string name="exporting">Exportuji…</string>
<string name="importOptionFilesystemTitle">Import z úložiště</string>
<string name="importOptionFilesystemExplanation">Vyberte konkrétní soubor v úložišti</string>
<string name="importOptionFilesystemExplanation">Vyberte konkrétní soubor v úložišti.</string>
<string name="importOptionFilesystemButton">Z úložiště</string>
<string name="about">O aplikaci</string>
<string name="app_license">Copyleftovaný svobodný software s licencí GPLv3+</string>
@@ -41,13 +41,13 @@
<string name="never">Nikdy</string>
<string name="expiryDate">Vypršení platnosti</string>
<string name="editBarcode">Upravit čárový kód</string>
<string name="app_resources">Zdroje třetích stran: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Knihovny třetích stran: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_resources">Svobodné zdroje třetích stran: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Svobodné knihovny třetích stran: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_copyright_old">Založeno na Loyalty Card Keychain
\ncopyright © 20162020 Branden Archer</string>
<string name="exportOptionExplanation">Data budou zapsána na místo podle vašeho výběru</string>
<string name="failedParsingImportUriError">Nepodařilo se zpracovat URI importu</string>
<string name="noCardExistsError">Kartu se nepodařilo najít</string>
<string name="exportOptionExplanation">Data budou zapsána na místo podle vašeho výběru.</string>
<string name="failedParsingImportUriError">Nelze zpracovat URI importu</string>
<string name="noCardExistsError">Tuto kartu nelze najít</string>
<string name="noCardsMessage">Nejprve přidejte kartu</string>
<string name="cardShortcut">Zástupce karty</string>
<string name="share">Sdílet</string>
@@ -83,12 +83,21 @@
<string name="expiryStateSentence">Platí do: <xliff:g>%s</xliff:g></string>
<string name="moveDown">Přesunout dolů</string>
<string name="moveUp">Přesunout nahoru</string>
<string name="settings_brown_theme">Hnědá</string>
<string name="settings_green_theme">Zelená</string>
<string name="settings_sky_blue_theme">Azurová</string>
<string name="settings_blue_theme">Modrá</string>
<string name="settings_violet_theme">Fialová</string>
<string name="settings_magenta_theme">Purpurová</string>
<string name="settings_pink_theme">Růžová</string>
<string name="settings_catima_theme">Catima</string>
<string name="settings_theme_color">Barva motivu</string>
<string name="settings_system_locale">Podle systému</string>
<string name="settings_locale">Jazyk</string>
<string name="turn_flashlight_off">Vypnout světlo</string>
<string name="turn_flashlight_on">Zapnout světlo</string>
<string name="failedGeneratingShareURL">Nepodařilo se vygenerovat adresu URL pro sdílení</string>
<string name="passwordRequired">Zadejte heslo</string>
<string name="failedGeneratingShareURL">Nepodařilo se vygenerovat adresu URL pro sdílení. Nahlaste to prosím.</string>
<string name="passwordRequired">Zadejte prosím heslo</string>
<string name="no">Ne</string>
<string name="yes">Ano</string>
<string name="updateBarcodeQuestionText">Změnili jste ID. Chcete také aktualizovat čárový kód, aby používal stejnou hodnotu\?</string>
@@ -106,29 +115,36 @@
<string name="barcodeId">Hodnota čárového kódu</string>
<string name="setBarcodeId">Nastavení hodnoty čárového kódu</string>
<string name="sameAsCardId">Stejné jako ID</string>
<string name="importVoucherVaultMessage">Vyberte soubor exportu z aplikace Voucher Vault, který chcete importovat.\nVytvořte jej z nabídky stisknutím tlačítka Export v aplikaci Voucher Vault.</string>
<string name="importVoucherVaultMessage">Vyberte k importu svůj <i>vouchervault.json</i> exportovaný z Voucher Vault.
\nVytvoříte jej tak, že nejprve stisknete tlačítko Exportovat v aplikaci Voucher Vault.</string>
<string name="importVoucherVault">Import z Voucher Vault</string>
<string name="importLoyaltyCardKeychainMessage">Vyberte soubor exportu z aplikace Loyalty Card Keychain, který chcete importovat.\nVytvořte jej z nabídky Import/export jiné aplikace Loyalty Card Keychain klepnutím na Export.</string>
<string name="importStocardMessage">Vyberte k importu svůj <i>***.zip</i> exportovaný z aplikace Stocard.
\nZískejte ji zasláním e-mailu na adresu support@stocardapp.com s žádostí o export vašich dat.</string>
<string name="importStocard">Import ze Stocard</string>
<string name="importLoyaltyCardKeychainMessage">Vyberte k importu <i>LoyaltyCardKeychain.csv</i> exportovaný z Loyalty Card Keychain.
\nVytvoříte jej z nabídky Import/Export v Loyalty Card Keychain tak, že tam nejprve stisknete tlačítko Exportovat.</string>
<string name="importLoyaltyCardKeychain">Import z Loyalty Card Keychain</string>
<string name="importFidmeMessage">Vyberte soubor exportu z aplikace FidMe, který chcete importovat, a poté ručně vyberte typy čárových kódů.\nVytvořte jej z aplikace FidMe vybráním položky Data Protection a stisknutím tlačítka Extract my data.</string>
<string name="importFidmeMessage">Vyberte k importu svůj <i>fidme-export-request-xxxxxx.zip</i> exportovaný z FidMe a poté vyberte typy čárových kódů ručně.
\nVytvoříte jej ze svého profilu FidMe tak, že nejprve zvolíte možnost Ochrana dat a poté stisknete tlačítko Extrahovat moje data.</string>
<string name="importFidme">Import z FidMe</string>
<string name="importCatimaMessage">Vyberte soubor exportu z aplikace Catima, který chcete importovat.\nVytvořte jej z nabídky Import/export jiné aplikace Catima klepnutím na Export.</string>
<string name="importCatimaMessage">Vyberte <i>catima.zip</i> exportovaný z aplikace Catima, který chcete importovat.
\nVytvoříte jej z nabídky Import/Export jiné aplikace Catima tak, že v ní nejprve stisknete tlačítko Exportovat.</string>
<string name="importCatima">Import z Catima</string>
<string name="accept">Přijmout</string>
<string name="privacy_policy">Ochrana soukromí</string>
<string name="privacy_policy">Zásady soukromí</string>
<string name="app_loyalty_card_keychain">Loyalty Card Keychain</string>
<string name="chooseImportType">Importovat data z</string>
<string name="points">Body</string>
<string name="currency">Měna</string>
<string name="balance">Zůstatek</string>
<string name="errorReadingImage">Nepodařilo se přečíst obrázek</string>
<string name="errorReadingImage">Obrázek se nepodařilo přečíst</string>
<string name="noBarcodeFound">Čárový kód nenalezen</string>
<string name="groupsList">Skupiny: <xliff:g>%s</xliff:g></string>
<string name="addFromImage">Vybrat obrázek z galerie</string>
<string name="addManually">Zadat čárový kód ručně</string>
<string name="leaveWithoutSaveConfirmation">Ukončit bez uložení\?</string>
<string name="leaveWithoutSaveTitle">Ukončit</string>
<string name="failedOpeningFileManager">Nepodařilo se otevřít správce souborů</string>
<string name="failedOpeningFileManager">Nejprve si nainstalujte správce souborů.</string>
<string name="deleteConfirmationGroup">Smazat skupinu\?</string>
<string name="all">Všechny</string>
<plurals name="groupCardCount">
@@ -136,7 +152,7 @@
<item quantity="few"><xliff:g>%d</xliff:g> karty</item>
<item quantity="other"><xliff:g>%d</xliff:g> karet</item>
</plurals>
<string name="noGroups">Klepnutím na tlačítko plus (+) přidejte skupiny pro kategorizaci</string>
<string name="noGroups">Kliknutím na tlačítko + plus přidejte skupiny pro kategorizaci.</string>
<string name="groups">Skupiny</string>
<string name="enter_group_name">Zadejte název skupiny</string>
<string name="exportSuccessful">Data exportována</string>
@@ -161,7 +177,7 @@
<string name="and_data_usage">a využití dat</string>
<string name="credits">Zásluhy</string>
<string name="on_github">na GitHubu</string>
<string name="source_repository">Zdrojový repozitář</string>
<string name="source_repository">Úložiště zdrojů</string>
<string name="license">Licence</string>
<string name="help_translate_this_app">Pomozte s překladem této aplikace</string>
<string name="report_error">Nahlásit chybu</string>
@@ -175,7 +191,7 @@
<string name="group_name_is_empty">Název skupiny nesmí být prázdný</string>
<string name="group_updated">Skupina aktualizována</string>
<string name="editGroup">Úprava skupiny: <xliff:g>%s</xliff:g></string>
<string name="noGiftCardsGroup">Vytvořte si karty a poté je zde přiřaďte do skupiny</string>
<string name="noGiftCardsGroup">Zatím nemáte žádné věrnostní karty. Jakmile nějaké přidáte, můžete je zde přiřadit do skupiny.</string>
<string name="shortcutSelectCard">Vybrat kartu</string>
<string name="translate_platform">na Weblate</string>
<string name="showMoreInfo">Zobrazit podrobnosti</string>
@@ -188,12 +204,17 @@
</plurals>
<string name="settings_oled_dark">Čistě černé pozadí pro tmavý motiv</string>
<string name="include_if_asking_support">Pokud chcete požádat o podporu, uveďte následující informace:</string>
<string name="settings_follow_system_orientation">Podle orientace systému</string>
<string name="settings_portrait_orientation">Na výšku</string>
<string name="settings_lock_on_opening_orientation">Ponechat orientaci jako při otevření karty</string>
<string name="archive">Archivovat</string>
<string name="unarchive">Vrátit z archivu</string>
<string name="unarchived">Karta vrácena z archivu</string>
<string name="settings_card_orientation">Orientace obrazovky</string>
<string name="settings_landscape_orientation">Na šířku</string>
<string name="duplicateCard">Duplikovat</string>
<string name="archived">Karta archivována</string>
<string name="failedLaunchingPhotoPicker">Nepodařilo se najít podporovaný nástroj pro výběr obrázků</string>
<string name="failedLaunchingPhotoPicker">Nepodařilo se najít podporovanou aplikaci galerie</string>
<plurals name="groupCardCountWithArchived">
<item quantity="one"><xliff:g>%1$d</xliff:g> karta (<xliff:g id="archivedCount">%2$d</xliff:g> archivovaná)</item>
<item quantity="few"><xliff:g>%1$d</xliff:g> karty (<xliff:g id="archivedCount">%2$d</xliff:g> archivované)</item>
@@ -205,7 +226,7 @@
<string name="welcome">Vítejte v Catima</string>
<string name="barcodeLongPressMessage">V aplikaci pro galerii mohou být otevírány pouze obrázky</string>
<string name="failedToRetrieveImageFile">Nepodařilo se získat soubor obrázku</string>
<string name="cameraPermissionDeniedTitle">Nepodařilo se získat přístup k fotoaparátu</string>
<string name="cameraPermissionDeniedTitle">Nelze získat přístup k fotoaparátu</string>
<string name="importCards">Importovat karty</string>
<string name="updateBalance">Aktualizovat zůstatek</string>
<string name="currentBalanceSentence">Současný zůstatek: <xliff:g>%s</xliff:g></string>
@@ -223,8 +244,8 @@
<string name="switchToFrontImage">Přepnout na přední obrázek</string>
<string name="switchToBackImage">Přepnout na zadní obrázek</string>
<string name="switchToBarcode">Přepnout na čárový kód</string>
<string name="openFrontImageInGalleryApp">Otevřít přední obrázek v aplikaci prohlížeče obrázků</string>
<string name="openBackImageInGalleryApp">Otevřít zadní obrázek v aplikaci prohlížeče obrázků</string>
<string name="openFrontImageInGalleryApp">Otevřít přední obrázek v galerii</string>
<string name="openBackImageInGalleryApp">Otevřít zadní obrázek v galerii</string>
<string name="setBarcodeHeight">Nastavit výšku čárového kódu</string>
<string name="donate">Přispět</string>
<string name="icon_header_click_text">Dlouhým stisknutím miniaturu upravíte</string>
@@ -257,17 +278,18 @@
<string name="addWithoutBarcode">Přidat kartu bez čárového kódu</string>
<string name="field_must_not_be_empty">Položka nesmí být prázdná</string>
<string name="app_name">Catima</string>
<string name="settings_follow_sensor_orientation">Vždy otáčet (ignoruje nastavení systému)</string>
<string name="continue_">Pokračovat</string>
<string name="add_manually_warning_title">Doporučuje se skenování</string>
<string name="add_manually_warning_message">U některých karet se hodnota čárového kódu liší od čísla napsaného na kartě. Z tohoto důvodu nemusí ruční zadání čárového kódu vždy fungovat. Doporučujeme místo toho naskenovat čárový kód pomocí fotoaparátu. Chcete přesto pokračovat?</string>
<string name="add_manually_warning_message">V některých obchodech se hodnota čárového kódu liší od čísla napsaného na kartě. Z tohoto důvodu nemusí ruční zadání čárového kódu vždy fungovat. Důrazně doporučujeme místo toho naskenovat čárový kód pomocí fotoaparátu. Chcete přesto pokračovat?</string>
<string name="spend">Utratit</string>
<string name="receive">Obdržet</string>
<string name="amountParsingFailed">Neplatné množství</string>
<string name="addFromPdfFile">Vybrat soubor PDF</string>
<string name="errorReadingFile">Soubor se nepodařilo přečíst</string>
<string name="errorReadingFile">Soubor nelze přečíst</string>
<string name="pageWithNumber">Stránka <xliff:g>%d</xliff:g></string>
<string name="multipleBarcodesFoundPleaseChooseOne">Který z nalezených čárových kódů chcete použít?</string>
<string name="failedLaunchingFileManager">Nepodařilo se nat podporovaného správce souborů</string>
<string name="failedLaunchingFileManager">Nelze nalézt podporovaný správce souborů</string>
<string name="noCameraFoundGuideText">Zdá se, že vaše zařízení nemá fotoaparát. Pokud ano, zkuste zařízení restartovat. V opačném případě použijte tlačítko Další možnosti a přidejte čárový kód jiným způsobem.</string>
<string name="importCancelled">Import zrušen</string>
<string name="exportCancelled">Export zrušen</string>
@@ -275,10 +297,10 @@
<string name="useFrontImage">Použít přední obrázek</string>
<string name="settings_use_volume_keys_navigation_summary">Pomocí tlačítek hlasitosti můžete změnit, která karta se zobrazí</string>
<string name="settings_use_volume_keys_navigation">Přepínat karty pomocí tlačítek hlasitosti</string>
<string name="generic_error_please_retry">Došlo k chybě</string>
<string name="generic_error_please_retry">Je nám líto, něco se pokazilo, zkuste to prosím znovu...</string>
<string name="settings_column_count_portrait">Sloupce v režimu na výšku</string>
<string name="settings_automatic_column_count">Automatický</string>
<string name="addFromPkpass">Vyberte soubor Passbook (.pkpass / .pkpasses)</string>
<string name="addFromPkpass">Vyberte soubor Passbook (.pkpass)</string>
<string name="unsupportedFile">Tento soubor není podporován</string>
<string name="settings_category_title_cards_overview">Přehled karet</string>
<string name="settings_column_count_landscape">Sloupce v režimu na šířku</string>
@@ -294,15 +316,4 @@
<string name="width">Šířka</string>
<string name="card_list_widget_name">Seznam karet</string>
<string name="card_list_widget_empty">Karty přidané do aplikace Catima se zobrazí zde. Pokud máte karty, ujistěte se, že nejsou všechny archivovány.</string>
<string name="cardWithNumber">Karta <xliff:g>%d</xliff:g></string>
<string name="cardWithNumberAndLocale">Karta <xliff:g>%d</xliff:g> (<xliff:g>%s</xliff:g>)</string>
<string name="pleaseDoNotRotateTheDevice">Neotáčejte prosím zařízení, protože tím zrušíte akci</string>
<string name="acra_catima_has_crashed">Omlouváme se, aplikace <xliff:g id="app_name">%s</xliff:g> havarovala. Pomozte nám prosím s opravou tohoto problému odesláním hlášení o chybě.</string>
<string name="acra_explain_crash">Pokud je to možné, přidejte prosím další podrobnosti o tom, co jste tu dělali:</string>
<string name="acra_crash_email_subject">Hlášení o pádu <xliff:g id="app_name">%s</xliff:g></string>
<string name="pref_enable_acra">Ptát se na odesílání hlášení o pádech</string>
<string name="pref_enable_acra_summary">Pokud je povoleno, budete při pádu aplikace dotázáni na jeho nahlášení. Hlášení nejsou nikdy odesílána automaticky.</string>
<string name="copy_value">Kopírovat hodnotu</string>
<string name="copied_to_clipboard">Zkopírováno do schránky</string>
<string name="nothing_to_copy">Nenalezena žádná hodnota</string>
</resources>

View File

@@ -3,15 +3,15 @@
<string name="scanCardBarcode">Scan stregkode</string>
<string name="addCardTitle">Tilføj kort</string>
<string name="editCardTitle">Rediger kort</string>
<string name="sendLabel">Send…</string>
<string name="share">Del</string>
<string name="sendLabel">Afsend…</string>
<string name="share">Aktie</string>
<string name="ok">OK</string>
<string name="deleteConfirmation">Slet dette kort permanent?</string>
<string name="deleteConfirmation">Slete dette kort permanent\?</string>
<plurals name="deleteCardsTitle">
<item quantity="one">Slet <xliff:g>%d</xliff:g> kort</item>
<item quantity="other">Slet <xliff:g>%d</xliff:g> korts</item>
<item quantity="one">Streichen <xliff:g>%d</xliff:g> kort</item>
<item quantity="other">Streichen <xliff:g>%d</xliff:g> korts</item>
</plurals>
<string name="deleteTitle">Slet kort</string>
<string name="deleteTitle">Karte streichen</string>
<string name="confirm">Bekræft</string>
<string name="delete">Slet</string>
<string name="edit">Rediger</string>
@@ -34,7 +34,7 @@
<string name="action_search">Søg</string>
<string name="importExport">Import/eksport</string>
<string name="exportName">Eksport</string>
<string name="importExportHelp">Sikkerhedskopiering af dine data, giver dig mulighed for at flytte dem til en anden enhed.</string>
<string name="importExportHelp">Sikkerhedskopiering af dit data, giver dig mulighed for at flytte dem til en anden enhed.</string>
<string name="importSuccessfulTitle">Importeret</string>
<string name="importFailedTitle">Import mislykkedes</string>
<string name="importFailed">Kunne ikke udføre importering</string>
@@ -54,12 +54,12 @@
\ncopyright © 2016-2020 Branden Archer.</string>
<string name="about">Om</string>
<string name="noCardsMessage">Tilføj først et kort</string>
<string name="cardShortcut">Genvej til kort</string>
<string name="cardShortcut">Kort genvej</string>
<string name="importOptionFilesystemButton">Fra filsystemet</string>
<string name="importOptionFilesystemExplanation">Vælg en bestemt fil fra filsystemet.</string>
<string name="importOptionFilesystemTitle">Import fra filsystem</string>
<string name="exportOptionExplanation">Dataene skrives til en placering efter eget valg.</string>
<string name="failedParsingImportUriError">Kunne ikke importere URI\'en</string>
<string name="failedParsingImportUriError">Kunne ikke analysere import-URI\'en</string>
<string name="noCardExistsError">Kunne ikke finde det kort</string>
<string name="deleteConfirmationGroup">Slet gruppe\?</string>
<string name="all">Alle</string>
@@ -79,16 +79,16 @@
<string name="moveDown">Bevæger sig nedad</string>
<string name="leaveWithoutSaveTitle">Afslut</string>
<string name="addManually">Indtast stregkoden manuelt</string>
<string name="noGiftCardsGroup">Opret kort og tildel dem grupper her.</string>
<string name="noGiftCardsGroup">Opret kort og tildel dem gupper her.</string>
<plurals name="deleteCardsConfirmation">
<item quantity="one">Slet dette <xliff:g>%d</xliff:g> kort permanent\?</item>
<item quantity="other">Slet disse <xliff:g>%d</xliff:g> kort permanent\?</item>
</plurals>
<string name="app_name">Catima</string>
<string name="cameraPermissionRequired">Behov for kamera adgang er krævet for denne funktion…</string>
<string name="storageReadPermissionRequired">Behov for lager adgang er krævet for denne funktion…</string>
<string name="cameraPermissionRequired">Behov for kamera adgang krævet for denne funktion…</string>
<string name="storageReadPermissionRequired">Behov for lager adgang krævet for denne funktion…</string>
<string name="permissionReadCardsLabel">Læs Catima Kort</string>
<string name="permissionReadCardsDescription">læs dit Catima kort og alle kortets detaljer, også noter og billeder</string>
<string name="permissionReadCardsDescription">læs dine Catima kort og alle deres detaljer, også noter og billeder</string>
<string name="cameraPermissionDeniedTitle">Kunne ikke få adgang til kamera</string>
<string name="noCameraPermissionDirectToSystemSetting">For at scanne stregkoder, har Catima behov for at få adgang til dit kamera. Klik her for at ændre dine tilladelser i indstillinger.</string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Copyright © 2019<xliff:g>%d</xliff:g> Sylvia van Os og hjælpere</string>
@@ -100,6 +100,10 @@
<string name="group_name_already_in_use">Gruppenavn allerede i brug</string>
<string name="editGroup">Redigerer Gruppe: <xliff:g>%s</xliff:g></string>
<string name="importFidme">Importer fra FidMe</string>
<string name="settings_card_orientation">Skærm orientation</string>
<string name="settings_follow_system_orientation">Følg system</string>
<string name="settings_portrait_orientation">Portræt</string>
<string name="settings_landscape_orientation">Landskab</string>
<string name="settings_disable_lockscreen_while_viewing_card_summary">Deaktiver låseskærm når et kort er åbent</string>
<string name="groupsList">Grupper: <xliff:g>%s</xliff:g></string>
<string name="expiryStateSentence">Udløber: <xliff:g>%s</xliff:g></string>
@@ -110,6 +114,7 @@
<string name="never">Aldrig</string>
<string name="chooseExpiryDate">Vælg udløbsdato</string>
<string name="balance">Balance</string>
<string name="importStocard">Importer fra Stocard</string>
<string name="balanceSentence">Balance: <xliff:g>%s</xliff:g></string>
<string name="group_name_is_empty">Gruppenavn må ikke være tom</string>
<string name="group_updated">Gruppe opdateret</string>
@@ -129,8 +134,10 @@
<string name="setBarcodeId">Vælg stregkode værdi</string>
<string name="sameAsCardId">Samme som ID</string>
<string name="settings_system_theme">System</string>
<string name="settings_lock_on_opening_orientation">Lås til orientation når kort åbnes</string>
<string name="settings_keep_screen_on_summary">Deaktiver skærm tids slukning når et kort er åbent</string>
<string name="group_edit">Rediger gruppe</string>
<string name="settings_follow_sensor_orientation">Altid roter (ignorer system indstillinger)</string>
<string name="chooseImportType">Importer data fra</string>
<string name="importVoucherVault">Importer fra Voucher Vault</string>
<string name="settings_use_volume_keys_navigation">Skift kort ved brug af lydstyrke knapperne</string>
@@ -144,4 +151,4 @@
<item quantity="one"><xliff:g>%s</xliff:g> point</item>
<item quantity="other"><xliff:g>%s</xliff:g> point</item>
</plurals>
</resources>
</resources>

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" xmlns:tools="http://schemas.android.com/tools">
<string name="action_search">Suche</string>
<string name="action_search">Suchen</string>
<string name="action_add">Hinzufügen</string>
<string name="noGiftCards">Füge eine Karte mit + hinzu oder importiere welche über das ⋮ Menü</string>
<string name="noGiftCards">Füge eine Karte mit + hinzu oder importiere welche über das ⋮ Menü.</string>
<string name="noMatchingGiftCards">Keine Ergebnisse. Versuche, deine Suche zu ändern.</string>
<string name="storeName">Name</string>
<string name="note">Notiz</string>
@@ -24,7 +24,7 @@
<string name="noCardExistsError">Konnte die Karte nicht finden</string>
<string name="importExport">Import/Export</string>
<string name="exportName">Export</string>
<string name="importExportHelp">Wenn du deine Daten sicherst, kannst du sie auf ein anderes Gerät übertragen</string>
<string name="importExportHelp">Wenn du deine Daten sicherst, kannst du sie auf ein anderes Gerät übertragen.</string>
<string name="importSuccessfulTitle">Importiert</string>
<string name="importFailedTitle">Import fehlgeschlagen</string>
<string name="importFailed">Import konnte nicht durchgeführt werden</string>
@@ -34,7 +34,7 @@
<string name="importing">Importiere…</string>
<string name="exporting">Exportiere…</string>
<string name="importOptionFilesystemTitle">Aus Dateisystem importieren</string>
<string name="importOptionFilesystemExplanation">Wähle eine bestimmte Datei aus dem Dateisystem aus</string>
<string name="importOptionFilesystemExplanation">Wähle eine bestimmte Datei aus dem Dateisystem aus.</string>
<string name="importOptionFilesystemButton">vom Dateisystem</string>
<string name="about">Über</string>
<string name="app_license">Freie Software, lizensiert unter der GPLv3+</string>
@@ -53,20 +53,20 @@
<string name="settings_theme">Farbschema</string>
<string name="app_copyright_old">Basierend auf Loyalty Card Keychain
\nCopyright © 2016-2020 Branden Archer</string>
<string name="exportOptionExplanation">Die Daten werden an einen Ort deiner Wahl geschrieben</string>
<string name="exportOptionExplanation">Die Daten werden an einen Ort deiner Wahl geschrieben.</string>
<string name="failedParsingImportUriError">Die Import-URI konnte nicht verarbeitet werden</string>
<string name="share">Teilen</string>
<string name="barcodeType">Barcodetyp</string>
<string name="starImage">Favoritenstern</string>
<string name="deleteConfirmationGroup">Gruppe löschen?</string>
<string name="all">Alle</string>
<string name="noGroups">Klicke auf das Pluszeichen +, um eine Gruppe hinzuzufügen</string>
<string name="noGroups">Klicke auf das Pluszeichen +, um eine Gruppe hinzuzufügen.</string>
<string name="noGroupCards">Diese Gruppe ist leer</string>
<string name="groups">Gruppen</string>
<string name="enter_group_name">Gruppennamen eingeben</string>
<string name="leaveWithoutSaveConfirmation">Beenden ohne zu speichern\?</string>
<string name="leaveWithoutSaveTitle">Beenden</string>
<string name="failedOpeningFileManager">Dateimanager konnte nicht geöffnet werden</string>
<string name="failedOpeningFileManager">Installiere zuerst einen Dateimanager.</string>
<string name="noBarcode">Kein Barcode</string>
<string name="addManually">Barcode manuell eingeben</string>
<string name="moveDown">Nach unten verschieben</string>
@@ -94,13 +94,14 @@
<string name="settings_keep_screen_on">Bildschirm aktiv lassen</string>
<string name="accept">Annehmen</string>
<string name="privacy_policy">Datenschutzrichtlinie</string>
<string name="importVoucherVaultMessage">Wähle deinen Export aus Voucher Vault zum Importieren aus.\nErstelle ihn, indem du Export in Voucher Vault drückst.</string>
<string name="importVoucherVaultMessage">Wähle deinen <i>vouchervault.json</i>-Export aus Voucher Vault zum Importieren aus. \nErstelle ihn, indem du zuerst auf Export in Voucher Vault drückst.</string>
<string name="importVoucherVault">Aus Voucher Vault importieren</string>
<string name="importLoyaltyCardKeychainMessage">Wähle deinen Export vom „Loyalty Card Keychain“ zum Importieren aus.\nErstelle ihn über das Import/Export Menü in Loyalty Card Keychain, indem du dort auf Export drückst.</string>
<string name="importLoyaltyCardKeychainMessage">Wählen du deinen <i>LoyaltyCardKeychain.csv</i>-Export aus Loyalty Card Keychain zum Importieren aus.
\nErstelle ihn über das Menü Import/Export in Loyalty Card Keychain, indem du dort zuerst auf Export drückst.</string>
<string name="importLoyaltyCardKeychain">Aus Loyalty Card Keychain importieren</string>
<string name="importFidmeMessage">Wähle deinen „FidMe-Export“ zum Importieren und anschließend manuell die Barcodetypen aus.\nErstelle ihn aus deinem FidMe-Profil, indem du Datenschutz wählst und dann auf Meine Daten extrahieren drückst.</string>
<string name="importFidmeMessage">Wähle deinen <i>fidme-export-request-xxxxxx.zip</i>-Export aus FidMe zum Importieren aus und wähle anschließend die Barcodetypen manuell aus. \nOder erstelle ihn aus deinem FidMe-Profil, indem du Datenschutz wählst und dann zuerst auf Meine Daten extrahieren drückst.</string>
<string name="importFidme">Aus FidMe importieren</string>
<string name="importCatimaMessage">Wähle deinen „Catima-Export“ zum Importieren aus.\nErstelle diesen durch das Drücken auf Export im Import/Export-Menü in einer anderen Catima-Anwendung.</string>
<string name="importCatimaMessage">Wähle deinen „<i>catima.zip</i>-Export“ von Catima zum Importieren aus.\nErstelle ihn zuerst aus dem Import/Export-Menü einer anderen Catima-Anwendung, indem du dort Export drückst.</string>
<string name="importCatima">Aus Catima importieren</string>
<string name="setBarcodeId">Barcodewert festlegen</string>
<string name="sameAsCardId">Entspricht Kartennummer</string>
@@ -109,9 +110,9 @@
<string name="noBarcodeFound">Keinen Barcode erkannt</string>
<string name="addFromImage">Bild aus der Galerie wählen</string>
<string name="unsupportedBarcodeType">Dieser Barcodetyp kann noch nicht angezeigt werden. Wir hoffen das Format in einer zukünftigen Version zu unterstützen.</string>
<string name="wrongValueForBarcodeType">Der Wert ist ungültig für den gewählten Barcodetyp</string>
<string name="app_resources">Ressourcen von Drittanbietern: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Bibliotheken von Drittanbietern: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="wrongValueForBarcodeType">Der Wert ist für den gewählten Barcodetyp leider nicht gültig</string>
<string name="app_resources">Freie Ressourcen von Drittanbietern: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Freie Bibliotheken von Drittanbietern: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="intent_import_card_from_url_share_multiple_text">Ich möchte diese Karten mit dir teilen</string>
<string name="no">Nein</string>
<string name="yes">Ja</string>
@@ -124,10 +125,12 @@
<string name="photos">Fotos</string>
<string name="frontImageDescription">Vorderseite</string>
<string name="backImageDescription">Rückseite</string>
<string name="passwordRequired">Gib das Passwort ein</string>
<string name="passwordRequired">Bitte gib das Passwort ein</string>
<string name="importStocardMessage">Wähle deinen <i>***.zip</i>-Export aus Stocard zum Importieren aus. \nDu erhälst ihn, indem du eine E-Mail an support@stocardapp.com sendest und um einen Export deiner Daten bittest.</string>
<string name="importStocard">Von Stocard importieren</string>
<string name="turn_flashlight_off">Blitzlicht ausschalten</string>
<string name="turn_flashlight_on">Blitzlicht einschalten</string>
<string name="failedGeneratingShareURL">Teilbare URL konnte nicht erstellt werden</string>
<string name="failedGeneratingShareURL">URL konnte nicht erstellt werden. Bitte melde das an uns.</string>
<plurals name="selectedCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> ausgewählt</item>
<item quantity="other"><xliff:g>%d</xliff:g> ausgewählt</item>
@@ -144,6 +147,15 @@
</plurals>
<string name="settings_system_locale">System</string>
<string name="settings_locale">Sprache</string>
<string name="settings_brown_theme">Braun</string>
<string name="settings_green_theme">Grün</string>
<string name="settings_sky_blue_theme">Himmelblau</string>
<string name="settings_blue_theme">Blau</string>
<string name="settings_violet_theme">Violett</string>
<string name="settings_magenta_theme">Magenta</string>
<string name="settings_pink_theme">Rosa</string>
<string name="settings_catima_theme">Catima</string>
<string name="settings_theme_color">Designfarbe</string>
<string name="app_contributors">Ermöglicht durch: <xliff:g id="app_contributors">%s</xliff:g></string>
<string name="barcodeImageDescriptionWithType">Bild <xliff:g>%s</xliff:g> Barcode</string>
<string name="sort_by">Sortieren nach</string>
@@ -167,9 +179,9 @@
<string name="group_name_already_in_use">Der Gruppenname wird bereits verwendet</string>
<string name="group_name_is_empty">Gruppenname darf nicht leer sein</string>
<string name="group_updated">Gruppe aktualisiert</string>
<string name="editGroup">Gruppe bearbeiten: <xliff:g>%s</xliff:g></string>
<string name="editGroup">Gruppe wird bearbeitet: <xliff:g>%s</xliff:g></string>
<string name="group_edit">Gruppe bearbeiten</string>
<string name="noGiftCardsGroup">Erstelle einige Karten und ordne sie dann hier der Gruppe zu</string>
<string name="noGiftCardsGroup">Erstelle einige Karten und ordne sie dann hier der Gruppe zu.</string>
<string name="setIcon">Vorschaubild festlegen</string>
<string name="selectColor">Farbe auswählen</string>
<string name="translate_platform">auf Weblate</string>
@@ -183,16 +195,21 @@
</plurals>
<string name="settings_oled_dark">Komplett schwarzer Hintergrund im dunklen Design</string>
<string name="include_if_asking_support">Wenn Du Unterstützung haben möchtest, gib bitte folgende Informationen an:</string>
<string name="settings_follow_system_orientation">System folgen</string>
<string name="settings_landscape_orientation">Querformat</string>
<string name="settings_portrait_orientation">Hochformat</string>
<string name="duplicateCard">Duplizieren</string>
<string name="unarchive">Aus dem Archiv wiederherstellen</string>
<string name="settings_card_orientation">Bildschirm-Ausrichtung</string>
<string name="unarchived">Karte aus dem Archiv wiederhergestellt</string>
<string name="archive">Archivieren</string>
<string name="archived">Karte archiviert</string>
<string name="settings_lock_on_opening_orientation">Kartenausrichtung nach dem Öffnen beibehalten</string>
<plurals name="groupCardCountWithArchived">
<item quantity="one"><xliff:g>%1$d</xliff:g> Karte (<xliff:g id="archivedCount">%2$d</xliff:g> archiviert)</item>
<item quantity="other"><xliff:g>%1$d</xliff:g> Karten (<xliff:g id="archivedCount">%2$d</xliff:g> archiviert)</item>
</plurals>
<string name="failedLaunchingPhotoPicker">Keine unterstützte App zur Bildauswahl gefunden</string>
<string name="failedLaunchingPhotoPicker">Es konnte keine unterstützte Galerie-App gefunden werden</string>
<string name="previousCard">Vorherige</string>
<string name="nextCard">Nächste</string>
<string name="failedToOpenUrl">Bitte installiere zuerst einen Webbrowser</string>
@@ -200,7 +217,7 @@
<string name="barcodeLongPressMessage">In der Galerie können nur Bilder geöffnet werden</string>
<string name="failedToRetrieveImageFile">Bilddatei konnte nicht abgerufen werden</string>
<string name="updateBalanceTitle">Wie viel hast du ausgegeben oder erhalten?</string>
<string name="cameraPermissionDeniedTitle">Kein Zugriff auf die Kamera</string>
<string name="cameraPermissionDeniedTitle">Kein Zugriff auf die Kamera möglich</string>
<string name="noCameraPermissionDirectToSystemSetting">Um Barcodes zu scannen, benötigt Catima Zugriff auf deine Kamera. Tippe hier, um deine Berechtigungseinstellungen zu ändern.</string>
<string name="updateBalanceHint">Betrag eingeben</string>
<string name="importCards">Karten importieren</string>
@@ -215,8 +232,8 @@
<string name="anyDate">Beliebiges Datum</string>
<string name="icon_header_click_text">Zum Bearbeiten des Vorschaubildes lang drücken</string>
<string name="switchToBarcode">Zum Barcode wechseln</string>
<string name="openFrontImageInGalleryApp">Vorderseite in Bildbetrachter öffnen</string>
<string name="openBackImageInGalleryApp">Rückseite in Bildbetrachter öffnen</string>
<string name="openFrontImageInGalleryApp">Vorderseite in Galerie öffnen</string>
<string name="openBackImageInGalleryApp">Rückseite in Galerie öffnen</string>
<string name="height">Höhe</string>
<string name="switchToFrontImage">Zur Vorderseite wechseln</string>
<string name="switchToBackImage">Zur Rückseite wechseln</string>
@@ -251,9 +268,10 @@
<string name="field_must_not_be_empty">Feld darf nicht leer sein</string>
<string name="manually_enter_barcode_instructions">Trage die Kartenummer oder Text deiner Karte ein und drücke auf den Barcode, der wie der auf deiner Karte aussieht.</string>
<string name="app_name">Catima</string>
<string name="settings_follow_sensor_orientation">Immer drehen (ignoriert Systemeinstellungen)</string>
<string name="continue_">Fortfahren</string>
<string name="add_manually_warning_title">Scannen empfohlen</string>
<string name="add_manually_warning_message">In einigen Geschäften weicht der Wert des Barcodes von dem auf der Karte angegebenen Wert ab. Aus diesem Grund funktioniert die manuelle Eingabe des Barcodes in einigen Fällen nicht. Es wird empfohlen, stattdessen den Barcode mit deiner Kamera zu scannen. Möchtest du dennoch fortfahren?</string>
<string name="add_manually_warning_message">In einigen Geschäften weicht der Wert des Barcodes von dem auf der Karte angegebenen Wert ab. Aus diesem Grund funktioniert die manuelle Eingabe des Barcodes in einigen Fällen nicht. Es wird dringend empfohlen, den Barcode mit einer Kamera zu scannen. Möchtest du dennoch fortfahren?</string>
<string name="spend">Zahlen</string>
<string name="receive">Erhalten</string>
<string name="amountParsingFailed">Ungültiger Betrag</string>
@@ -280,23 +298,12 @@
<string name="settings_column_count_4">4</string>
<string name="settings_column_count_5">5</string>
<string name="settings_column_count_6">6</string>
<string name="generic_error_please_retry">Ein Fehler ist aufgetreten</string>
<string name="generic_error_please_retry">Entschuldigung, da ist etwas schief gelaufen, versuchen Sie es noch einmal ...</string>
<string name="unsupportedFile">Diese Datei wird nicht unterstützt</string>
<string name="addFromPkpass">Eine Passbook-Datei (.pkpass / .pkpasses) auswählen</string>
<string name="addFromPkpass">Passbook-Datei (.pkpass) auswählen</string>
<string name="sort_by_valid_from">Gültig ab</string>
<string name="width">Breite</string>
<string name="setBarcodeWidth">Barcodebreite einstellen</string>
<string name="card_list_widget_empty">Nachdem du einige Treuekarten in Catima hinzugefügt hast, werden sie hier angezeigt. Wenn du Karten hast, stelle sicher, dass diese nicht alle archiviert sind.</string>
<string name="card_list_widget_empty">Nachdem du einige Treuekarten in Catima hinzugefügt hast, werden sie hier angezeigt. Wenn du Karten hast, stelle sicher, dass sie nicht alle archiviert sind.</string>
<string name="card_list_widget_name">Kartenliste</string>
<string name="cardWithNumberAndLocale">Karte <xliff:g>%d</xliff:g> (<xliff:g>%s</xliff:g>)</string>
<string name="cardWithNumber">Karte <xliff:g>%d</xliff:g></string>
<string name="pref_enable_acra_summary">Wenn aktiviert, wirst du bei einem Absturz gebeten diesen zu melden. Absturzberichte werden niemals automatisch gesendet.</string>
<string name="pref_enable_acra">Bitte um die Übermittlung von Absturzberichten</string>
<string name="acra_crash_email_subject"><xliff:g id="app_name">%s</xliff:g> Absturzbericht</string>
<string name="acra_explain_crash">Wenn möglich, bitte übermittle mehr Details zu dem, was du hier getan hast:</string>
<string name="acra_catima_has_crashed">Es tut uns leid, aber <xliff:g id="app_name">%s</xliff:g> ist abgestürzt. Bitte hilf uns diesen Fehler zu beheben und übermittle uns einen Absturzbericht.</string>
<string name="pleaseDoNotRotateTheDevice">Bitte drehe nicht das Gerät, weil sonst die Aktion abbricht</string>
<string name="copy_value">Kopiere Betrag</string>
<string name="copied_to_clipboard">In die Zwischenablage kopiert</string>
<string name="nothing_to_copy">Keinen Betrag gefunden</string>
</resources>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" xmlns:tools="http://schemas.android.com/tools">
<string name="action_add">Προσθήκη</string>
<string name="noGiftCards">Κάντε κλικ στο + κουμπί για να προσθέσετε μία κάρτα ή προσθέστε από το ⋮ μενού</string>
<string name="noGiftCards">Κάντε κλικ στο + κουμπί για να προσθέσετε μία κάρτα ή προσθέστε από το ⋮ μενού.</string>
<string name="storeName">Όνομα</string>
<string name="note">Σημείωση</string>
<string name="cardId">Κωδικός κάρτας</string>
@@ -12,15 +12,15 @@
<string name="confirm">Επιβεβαίωση</string>
<string name="ok">Εντάξει</string>
<string name="sendLabel">Αποστολή…</string>
<string name="editCardTitle">Επεξεργασία κάρτας</string>
<string name="addCardTitle">Προσθήκη κάρτας</string>
<string name="scanCardBarcode">Σάρωση γραμμωτού κώδικα</string>
<string name="cardShortcut">Συντόμευση κάρτας</string>
<string name="editCardTitle">Επεξεργασία Κάρτας</string>
<string name="addCardTitle">Προσθήκη Κάρτας</string>
<string name="scanCardBarcode">Σαρώστε τον γραμμωτό κώδικα</string>
<string name="cardShortcut">Συντόμευση Κάρτας</string>
<string name="noCardsMessage">Προσθέστε μία κάρτα πρώτα</string>
<string name="noCardExistsError">Δεν ήταν δυνατό να εντοπιστεί η κάρτα</string>
<string name="importExport">Εισαγωγή/Εξαγωγή</string>
<string name="exportName">Εξαγωγή</string>
<string name="importExportHelp">Τα αντίγραφα ασφαλείας σας επιτρέπουν να τα εισάγετε σε άλλη συσκευή</string>
<string name="importExportHelp">Τα αντίγραφα ασφαλείας σας επιτρέπουν να τα εισάγετε σε άλλη συσκευή.</string>
<string name="importSuccessfulTitle">Εισήχθησαν</string>
<string name="importFailedTitle">Εισαγωγή ανεπιτυχής</string>
<string name="importFailed">Δεν ήταν δυνατή η εισαγωγή</string>
@@ -30,7 +30,7 @@
<string name="importing">Γίνεται εισαγωγή του…</string>
<string name="exporting">Γίνεται εξαγωγή του…</string>
<string name="importOptionFilesystemTitle">Εισαγωγή από το σύστημα αρχείων</string>
<string name="importOptionFilesystemExplanation">Επιλέξτε ένα συγκεκριμένο αρχείο από το σύστημα αρχείων</string>
<string name="importOptionFilesystemExplanation">Επιλέξτε ένα συγκεκριμένο αρχείο από το σύστημα αρχείων.</string>
<string name="importOptionFilesystemButton">Από το σύστημα αρχείων</string>
<string name="about">Σχετικά</string>
<string name="app_license">Άδεια χρήσης υπό GPLv3+</string>
@@ -50,7 +50,7 @@
<item quantity="one"><xliff:g>%d</xliff:g> επιλέγχθηκε</item>
<item quantity="other"><xliff:g>%d</xliff:g> επιλέγχθηκαν</item>
</plurals>
<string name="noGiftCardsGroup">Δημιούργησε κάρτες και βάλτες σε αυτή την ομάδα</string>
<string name="noGiftCardsGroup">Δημιούργησε κάρτες και βάλτες σε αυτή την ομάδα.</string>
<string name="addManually">Εισαγωγή γραμμωτού κώδικα με μη αυτόματο τρόπο</string>
<string name="never">Ποτέ</string>
<string name="share">Κοινοποίηση</string>
@@ -58,7 +58,7 @@
<item quantity="one"><xliff:g>%s</xliff:g> πόντος</item>
<item quantity="other"><xliff:g>%s</xliff:g> πόντοι</item>
</plurals>
<string name="exportOptionExplanation">Τα δεδομένα θα μεταφερθούν σε τοποθεσία της επιλογής σας</string>
<string name="exportOptionExplanation">Τα δεδομένα θα μεταφερθούν σε τοποθεσία της επιλογής σας.</string>
<string name="settings_theme">Θέμα</string>
<string name="groupsList">Ομάδες: <xliff:g>%s</xliff:g></string>
<string name="barcodeId">Τιμή γραμμωτού κώδικα</string>
@@ -81,13 +81,14 @@
<string name="source_repository">Αποθήκη κώδικα</string>
<string name="on_github">στο GitHub</string>
<string name="on_google_play">στο Google Play</string>
<string name="report_error">Αναφορά σφάλματος</string>
<string name="report_error">Αναφορά Σφάλματος</string>
<string name="starred">Αγαπημένα</string>
<string name="translate_platform">στο Weblate</string>
<string name="importLoyaltyCardKeychain">Εισαγωγή από Loyalty Card Keychain</string>
<string name="importLoyaltyCardKeychainMessage">Επιλέξτε την εξαγωγή σας από το Loyalty Card Keychain για εισαγωγή. \nΔημιουργήστε το από το μενού Εισαγωγής/Εξαγωγής στο Loyalty Card Keychain επιλέγοντας Εξαγωγή.</string>
<string name="importLoyaltyCardKeychainMessage">Επιλέξτε την <i>LoyaltyCardKeychain.csv</i> εξαγωγή από το Loyalty Card Keychain για εισαγωγή.
\nΔημιουργήστε το από το μενού Εισαγωγής/Εξαγωγής στο Loyalty Card Keychain επιλέγοντας Εξαγωγή.</string>
<string name="importFidme">Εισαγωγή από FidMe</string>
<string name="importFidmeMessage">Επιλέξτε την εξαγωγή σας από το FidMe για εισαγωγή και επιλέξτε χειροκίνητα τους τύπους γραμμωτού κώδικα.\nΔημιουργήστε το από το FidMe προφίλ επιλέγοντας Προστασία Δεδομένων και πατώντας Εξαγωγή δεδομένων.</string>
<string name="importFidmeMessage">Επιλέξτε την <i>fidme-export-request-xxxxxx.zip</i> εξαγωγή από το FidMe για εισαγωγή και επιλέξτε χειροκίνητα τους τύπους γραμμωτού κώδικα μετέπειτα.\nΔημιουργήστε το από το FidMe προφίλ επιλέγοντας Προστασία Δεδομένων και πατώντας Εξαγωγή δεδομένων πρώτα.</string>
<string name="setBarcodeId">Επιλέξτε τιμή γραμμωτού κώδικα</string>
<string name="wrongValueForBarcodeType">Η τιμή δεν είναι έγκυρη για τον επιλεγμένο γραμμωτό κώδικα</string>
<string name="setBackImage">Επιλογή οπίσθιας εικόνας</string>
@@ -105,7 +106,7 @@
<item quantity="one">Διαγραφή <xliff:g>%d</xliff:g> κάρτας</item>
<item quantity="other">Διαγραφή <xliff:g>%d</xliff:g> καρτών</item>
</plurals>
<string name="errorReadingImage">Δεν ήταν δυνατή η ανάγνωση της εικόνας</string>
<string name="errorReadingImage">Δεν ήταν δυνατό να διαβαστεί η εικόνα</string>
<string name="currency">Νόμισμα</string>
<string name="privacy_policy">Πολιτική απορρήτου</string>
<string name="chooseImportType">Εισαγωγή δεδομένων από</string>
@@ -114,29 +115,36 @@
<item quantity="one"><xliff:g>%1$d</xliff:g> κάρτα ( <xliff:g id="archivedCount">%2$d</xliff:g> αρχειοθετήθηκε)</item>
<item quantity="other"><xliff:g>%1$d</xliff:g> κάρτες ( <xliff:g id="archivedCount">%2$d</xliff:g> αρχειοθετήθηκαν)</item>
</plurals>
<string name="importCatimaMessage">Επιλέξτε την εξαγωγή σας από το Catima για εισαγωγή.\nΔημιουργήστε την από το μενού Εισαγωγή/Εξαγωγή μιας άλλης εφαρμογής Catima πατώντας Εξαγωγή.</string>
<string name="importCatimaMessage">Επιλέξτε την <i>catima.zip</i> εξαγωγή από το Catima για εισαγωγή
\nΔημιουργήστε το από το μενού Εισαγωγής/Εξαγωγής μιας άλλης εφαρμογής Catima κάνοντας εξαγωγή εκεί πρώτα.</string>
<string name="importStocardMessage">Επιλέξτε την <i>***.zip</i> εξαγωγή από το Stocard για εισαγωγή.
\nΠάρτε το στέλνοντας email στο: support@stocardapp.com ζητώντας μια εξαγωγή αρχείων των δεδομένων σας.</string>
<string name="intent_import_card_from_url_share_multiple_text">Θέλω να μοιραστώ μερικές κάρτες μαζί σου</string>
<string name="editGroup">Επεξεργασία ομάδας: <xliff:g>%s</xliff:g></string>
<string name="editGroup">Επεξεργασία Ομάδας: <xliff:g>%s</xliff:g></string>
<string name="setFrontImage">Επιλογή εμπρόσθιας εικόνας</string>
<string name="importVoucherVaultMessage">Επιλέξτε την εξαγωγή σας από το Voucher Vault για εισαγωγή. \nΔημιουργήστε το επιλέγοντας Εξαγωγή στο Voucher Vault.</string>
<string name="importVoucherVaultMessage">Επιλέξτε την <i>vouchervault.json</i> εξαγωγή από το Voucher Vault για εισαγωγή.
\nΔημιουργήστε το επιλέγοντας Εξαγωγή στο Voucher Vault.</string>
<string name="unsupportedBarcodeType">Ο τύπος γραμμωτού κώδικα δεν μπορεί να εμφανιστεί ακόμα. Μπορεί να υποστηρίζεται σε μια μελλοντική έκδοση της εφαρμογής.</string>
<string name="frontImageDescription">Εμπρόσθια</string>
<string name="photos">Φωτογραφίες</string>
<string name="backImageDescription">Οπίσθια</string>
<string name="updateBarcodeQuestionTitle">Ενημέρωση τιμής γραμμωτού κώδικα;</string>
<string name="passwordRequired">Εισάγετε τον κωδικό</string>
<string name="sort_by_most_recently_used">Πρόσφατα χρησιμοποιημένα</string>
<string name="passwordRequired">Παρακαλώ εισάγετε τον κωδικό</string>
<string name="sort_by_most_recently_used">Χρήση</string>
<string name="shortcutSelectCard">Επιλέξτε μία κάρτα</string>
<string name="barcodeImageDescriptionWithType">Εικόνα <xliff:g>%s</xliff:g> γραμμωτού κώδικα</string>
<string name="app_libraries">Βιβλιοθήκες τρίτων: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_libraries">Ελεύθερες βιβλιοθήκες τρίτων: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="license">Άδεια</string>
<string name="include_if_asking_support">Αν θέλετε να ζητήσετε υποστήριξη, συμπεριλάβετε τις ακόλουθες πληροφορίες:</string>
<string name="importSuccessful">Δεδομένα εισήχθησαν</string>
<string name="moveUp">Προχώρα πάνω</string>
<string name="barcodeType">Τύπος γραμμωτού κώδικα</string>
<string name="app_resources">Πηγές τρίτων: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_resources">Ελεύθερες πηγές τρίτων: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="selectColor">Επιλογή χρώματος</string>
<string name="setIcon">Ορισμός εικονιδίου</string>
<string name="settings_sky_blue_theme">Γαλάζιο</string>
<string name="settings_green_theme">Πράσινο</string>
<string name="settings_brown_theme">Καφέ</string>
<string name="sort_by_expiry">Λήξη</string>
<plurals name="groupCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> κάρτα</item>
@@ -145,7 +153,7 @@
<string name="points">Πόντοι</string>
<string name="exportSuccessful">Δεδομένα εξήχθησαν</string>
<string name="settings_disable_lockscreen_while_viewing_card">Αποτροπή κλειδώματος οθόνης</string>
<string name="failedLaunchingPhotoPicker">Δεν βρέθηκε υποστηριζόμενος επιλογέας εικόνων</string>
<string name="failedLaunchingPhotoPicker">Δεν βρέθηκε υποστηριζόμενη εφαρμογή συλλογής</string>
<string name="noBarcode">Χωρίς γραμμωτό κώδικα</string>
<string name="starImage">Αγαπημένο αστέρι</string>
<string name="balanceSentence">Υπόλοιπο: <xliff:g>%s</xliff:g></string>
@@ -156,15 +164,20 @@
</plurals>
<string name="app_copyright_old">Βασισμένο στο Loyalty Card Keychain
\nπνευματικά δικαιώματα © 2016-2020 Branden Archer</string>
<string name="settings_follow_system_orientation">Ακολούθηση συστήματος</string>
<string name="settings_card_orientation">Προσανατολισμός οθόνης</string>
<string name="settings_portrait_orientation">Πορτραίτο</string>
<string name="settings_landscape_orientation">Οριζόντια</string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Πνευματικά δικαιώματα © 2019-<xliff:g>%d</xliff:g> Sylvia van Os</string>
<string name="settings_lock_on_opening_orientation">Κλείδωμα τρέχοντος προσανατολισμού όταν ανοίγει μία κάρτα</string>
<string name="intent_import_card_from_url_share_text">Θέλω να μοιραστώ μία κάρτα μαζί σου</string>
<string name="enter_group_name">Εισάγετε όνομα ομάδας</string>
<string name="groups">Ομάδες</string>
<string name="noGroups">Κάντε κλικ στο + κουμπί ώστε να προσθέσετε ομάδες για κατηγοριοποίηση</string>
<string name="noGroups">Κάντε κλικ στο + κουμπί ώστε να προσθέσετε ομάδες για κατηγοριοποίηση.</string>
<string name="group_name_already_in_use">Αυτό το όνομα υπάρχει ήδη</string>
<string name="group_updated">Η ομάδα ενημερώθηκε</string>
<string name="all">Όλες</string>
<string name="failedOpeningFileManager">Αποτυχία εκκίνησης διαχειριστή αρχείων</string>
<string name="failedOpeningFileManager">Εγκαταστήστε έναν διαχειριστή αρχείων πρώτα.</string>
<string name="leaveWithoutSaveConfirmation">Έξοδος χωρίς αποθήκευση;</string>
<string name="expiryStateSentenceExpired">Έληξε: <xliff:g>%s</xliff:g></string>
<string name="card">Κάρτα</string>
@@ -174,16 +187,23 @@
<string name="noBarcodeFound">Δεν βρέθηκε γραμμωτός κώδικα</string>
<string name="balance">Υπόλοιπο</string>
<string name="importCatima">Εισαγωγή από Catima</string>
<string name="importStocard">Εισαγωγή από Stocard</string>
<string name="importVoucherVault">Εισαγωγή από Voucher Vault</string>
<string name="sameAsCardId">Όπως ο κωδικός</string>
<string name="exportPassword">Προσθέστε έναν κωδικό για προστασία της εξαγωγής (προαιρετικά)</string>
<string name="exportPasswordHint">Εισαγωγή κωδικού</string>
<string name="failedGeneratingShareURL">Δεν ήταν δυνατή η δημιουργία κοινοποιούμενου URL</string>
<string name="failedGeneratingShareURL">Δεν ήταν δυνατή η δημιουργία κοινοποιούμενου URL. Παρακαλώ αναφέρετε το.</string>
<string name="turn_flashlight_on">Ενεργοποίηση φακού</string>
<string name="turn_flashlight_off">Απενεργοποίηση φακού</string>
<string name="settings_locale">Γλώσσα</string>
<string name="settings_oled_dark">Απόλυτο μαύρο φόντο για το μαύρο θέμα</string>
<string name="settings_system_locale">Σύστημα</string>
<string name="settings_theme_color">Χρώμα θέματος</string>
<string name="settings_catima_theme">Κάτιμα</string>
<string name="settings_pink_theme">Ροζ</string>
<string name="settings_magenta_theme">Φούξια</string>
<string name="settings_violet_theme">Βιολετί</string>
<string name="settings_blue_theme">Μπλε</string>
<string name="app_contributors">Δημιουργήθηκε από: <xliff:g id="app_contributors">%s</xliff:g></string>
<string name="showMoreInfo">Εμφάνιση πληροφοριών</string>
<string name="sort_by_name">Όνομα</string>
@@ -225,6 +245,7 @@
<string name="icon_header_click_text">Πατήστε παρατεταμένα για επεξεργασία του εικονιδίου</string>
<string name="openFrontImageInGalleryApp">Ανοίξτε την εμπρόσθια εικόνα στη συλλογή εικόνων</string>
<string name="storageReadPermissionRequired">Δικαίωμα ανάγνωσης του χώρου αποθήκευσης απαραίτητο για αυτήν την ενέργεια…</string>
<string name="settings_follow_sensor_orientation">Πάντα σε περιστροφή (αγνοεί τις ρυθμίσεις του συστήματος)</string>
<string name="validFromDate">Ισχύει από</string>
<string name="anyDate">Οποιαδήποτε ημερομηνία</string>
<string name="chooseValidFromDate">Επιλέξτε έγκυρη ημερομηνία από</string>
@@ -235,7 +256,7 @@
<string name="continue_">Συνέχεια</string>
<string name="settings_category_title_privacy">Απόρρητο</string>
<string name="addFromPdfFile">Επιλογή αρχείου PDF</string>
<string name="add_manually_warning_message">Για ορισμένες κάρτες, ο γραμμωτός κώδικας διαφέρει από τον αριθμό που αναγράφεται πάνω στην κάρτα. Εξαιτίας αυτού, η εισαγωγή γραμμωτού κώδικα χειροκίνητα ενδέχεται να μην λειτουργεί πάντα. Προτείνεται να σαρώσετε τον γραμμωτό κώδικα με χρήση της κάμερας. Επιθυμείτε να συνεχίσετε;</string>
<string name="add_manually_warning_message">Για ορισμένα καταστήματα, ο γραμμωτός κώδικας διαφέρει από τον αριθμό που αναγράφεται πάνω στην κάρτα. Εξαιτίας αυτού, η εισαγωγή γραμμωτού κώδικα χειροκίνητα ενδέχεται να μην λειτουργεί πάντα. Προτείνεται να σκανάρετε τον γραμμωτό κώδικα με χρήση της κάμερας. Επιθυμείτε να συνεχίσετε ;</string>
<string name="amountParsingFailed">Μη έγκυρο ποσό</string>
<string name="show_balance">Προβολή υπολοίπου</string>
<string name="action_display_options">Επιλογές εμφάνισης</string>
@@ -280,23 +301,12 @@
<string name="settings_column_count_2">2</string>
<string name="settings_column_count_6">6</string>
<string name="settings_column_count_7">7</string>
<string name="generic_error_please_retry">Συνέβη ένα σφάλμα</string>
<string name="generic_error_please_retry">Λυπούμαστε, κάτι πήγε στραβά, δοκιμάστε ξανά...</string>
<string name="unsupportedFile">Το αρχείο δεν υποστηρίζεται</string>
<string name="addFromPkpass">Επιλέξτε αρχείο Passbook (.pkpass / .pkpasses)</string>
<string name="sort_by_valid_from">Έγκυρα από</string>
<string name="addFromPkpass">Επιλογή αρχείου Passbook (.pkpass)</string>
<string name="sort_by_valid_from">Έναρξη ισχύος</string>
<string name="setBarcodeWidth">Ορισμός πλάτους γραμμωτού κώδικα</string>
<string name="width">Πλάτος</string>
<string name="card_list_widget_empty">Αφού προσθέσετε μερικές κάρτες επιβράβευσης στο Catima, θα εμφανιστούν εδώ. Εάν έχετε κάρτες, βεβαιωθείτε ότι δεν είναι όλες αρχειοθετημένες.</string>
<string name="card_list_widget_name">Λίστα καρτών</string>
<string name="cardWithNumber">Κάρτα <xliff:g>%d</xliff:g></string>
<string name="cardWithNumberAndLocale">Κάρτα <xliff:g>%d</xliff:g> (<xliff:g>%s</xliff:g>)</string>
<string name="pleaseDoNotRotateTheDevice">Μην περιστρέφετε τη συσκευή, καθώς αυτό θα ακυρώσει την ενέργεια</string>
<string name="acra_catima_has_crashed">Λυπούμαστε, αλλά το <xliff:g id="app_name">%s</xliff:g> παρουσίασε σφάλμα. Βοηθήστε μας να διορθώσουμε αυτό το πρόβλημα, στέλνοντάς μας μια αναφορά σφάλματος.</string>
<string name="acra_explain_crash">Αν είναι δυνατόν, προσθέστε περισσότερες λεπτομέρειες σχετικά με το τι κάνατε εδώ:</string>
<string name="acra_crash_email_subject">Αναφορά σφάλματος <xliff:g id="app_name">%s</xliff:g></string>
<string name="pref_enable_acra">Ερώτηση για αποστολή αναφορών σφαλμάτων</string>
<string name="pref_enable_acra_summary">Όταν είναι ενεργοποιημένη, θα σας ζητηθεί να αναφέρετε ένα σφάλμα όταν συμβεί. Οι αναφορές σφάλματος δεν αποστέλλονται ποτέ αυτόματα.</string>
<string name="copy_value">Αντιγραφή τιμής</string>
<string name="copied_to_clipboard">Αντιγράφηκε στο πρόχειρο</string>
<string name="nothing_to_copy">Δεν βρέθηκε τιμή</string>
</resources>

View File

@@ -74,7 +74,7 @@
<string name="intent_import_card_from_url_share_text">Mi deziras dividi karto kun vi</string>
<string name="exportSuccessful">Datumoj eksportitaj</string>
<string name="noGroupCards">Ĉi tiu grupo estas malplena</string>
<string name="noGiftCards">Klavu la \"+\" butonon por aldoni karton, aŭ importu el la menuo \" ⋮\"</string>
<string name="noGiftCards">Klavu la \"+\" butonon por aldoni karton, aŭ importu el la menuo \" ⋮\".</string>
<plurals name="selectedCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> elektita</item>
<item quantity="other"><xliff:g>%d</xliff:g> elektitaj</item>
@@ -101,6 +101,7 @@
<string name="balance">Saldo</string>
<string name="moveBarcodeToTopOfScreen">Movi la strekodon al la supro de la ekrano</string>
<string name="errorReadingImage">Ne eblis legi bildon</string>
<string name="settings_brown_theme">Bruna</string>
<string name="showMoreInfo">Montri informojn</string>
<string name="on_github">sur GitHub</string>
<string name="archive">Enarkivigi</string>
@@ -126,6 +127,7 @@
<string name="validFromDate">Valida ekde</string>
<string name="accept">Akcepti</string>
<string name="app_loyalty_card_keychain">Loyalty Card Keychain</string>
<string name="settings_sky_blue_theme">Ĉielblua</string>
<string name="unarchive">Elarkivigi</string>
<string name="switchToBarcode">Ŝanĝi al strikodo</string>
<string name="currentBalanceSentence">Nuna saldo: <xliff:g>%s</xliff:g></string>
@@ -164,6 +166,7 @@
<string name="balanceParsingFailed">Nevalida saldo</string>
<string name="chooseImportType">Importi datumojn de</string>
<string name="importCatima">Importi el Catima</string>
<string name="settings_green_theme">Verda</string>
<string name="updateBalance">Ĝisdatigi saldon</string>
<string name="barcodeLongPressMessage">Nur bildoj povas esti malfermitaj en la galeria apo</string>
<string name="sort_by_name">Nomo</string>
@@ -210,6 +213,10 @@
<string name="cameraPermissionDeniedTitle">Fotilo neatingebla</string>
<string name="noCameraPermissionDirectToSystemSetting">Por skani strikodojn Catima bezonas atingorajton al via fotilo. Klaku ĉi tie por ŝanĝi viajn permesajn agordojn.</string>
<string name="app_copyright_short">Kopirajto © Sylvia van Os kaj kontribuantoj</string>
<string name="settings_card_orientation">Orientiĝo de strikodo</string>
<string name="settings_follow_system_orientation">Laŭ la sistemo</string>
<string name="settings_portrait_orientation">Vertikala</string>
<string name="settings_landscape_orientation">Horizontala</string>
<string name="settings_display_barcode_max_brightness_summary">Bezonata por ke iuj skaniloj funkciu</string>
<string name="unsupportedBarcodeType">Ne eblas montri ĉi tiun strikodspecon. Ĝi eble estos subtenata en posta versio de la apo.</string>
<string name="importVoucherVaultMessage">Elektu la <i>vouchervault.json</i> eksporton de Voucher Vault kiun vi volas importi.
@@ -222,8 +229,12 @@
<string name="settings_oled_dark_summary">Malpligrandigas baterian uzadon sur OLED-ekranoj</string>
<string name="selectColor">Elekti koloron</string>
<string name="setIcon">Starigi bildeton</string>
<string name="settings_catima_theme">Catima</string>
<string name="settings_pink_theme">Rozkolora</string>
<string name="field_must_not_be_empty">Kampo devas ne esti malplena</string>
<string name="manually_enter_barcode_instructions">Entajpu la identigilon aŭ tekston sur via karto kaj premu la strikodon kiu aspektas kiel tiu sur via karto.</string>
<string name="importStocardMessage">Elektu la <i>***.zip</i> eksoporton de Stocard kiun vi volas importi.
\nAkiru ĝin sendante retpoŝton al support@stocardapp.com petante eksporton de viaj datumoj.</string>
<string name="turn_flashlight_off">Malŝalti poŝlampon</string>
<string name="add_manually_warning_title">Skani estas rekomendata</string>
<string name="continue_">Daŭrigi</string>
@@ -236,6 +247,7 @@
<string name="wrongValueForBarcodeType">La valoro ne validas por la elektita tipo de strikodo</string>
<string name="importCancelled">Importado nuligita</string>
<string name="exportCancelled">Eksportado nuligita</string>
<string name="settings_theme_color">Koloro de la temo</string>
<string name="app_libraries">Liberaj triaj bibliotekoj: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="addFromPdfFile">Elekti PDF-dosieron</string>
<string name="failedLaunchingFileManager">Subtenata dosiermastrumilo ne trovebla</string>
@@ -244,6 +256,8 @@
<string name="pageWithNumber">Paĝo <xliff:g>%d</xliff:g></string>
<string name="settings_system_locale">Sistemo</string>
<string name="app_resources">Liberaj triaj risurcoj: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="settings_follow_sensor_orientation">Ĉiam turni (ignori la agordojn de la sistemo)</string>
<string name="settings_lock_on_opening_orientation">Fiksi al la orientiĝo uzata dum malfermado de la karto</string>
<string name="importCatimaMessage">Elektu la <i>catima.zip</i> eksporton kiun vi volas importi.
\nKreu ĝin unue en la importi/eksporti menuo en alia Catima apo elektante \'eksporti\' tie.</string>
<string name="importFidme">Importi el FidMe</string>
@@ -258,9 +272,13 @@
<string name="updateBarcodeQuestionText">Vi ŝanĝis la identigon. Ĉu vi volas ankaŭ ĝisdatigi la strikodon por uzi la saman valoron?</string>
<string name="importLoyaltyCardKeychainMessage">Elektu la <i>LoyaltyCardKeychain.csv</i> eksporton de Loyalty Card Keychain kiun vi volas importi.
\nKreu ĝin unue de la \"Importi/eksporti\" menuo en Loyalty Card Keychain elektante \"eksporti\" tie.</string>
<string name="importStocard">Importi de Stocard</string>
<string name="importVoucherVault">Importi el Voucher Vault</string>
<string name="turn_flashlight_on">Enŝalti poŝlampon</string>
<string name="settings_locale">Lingvo</string>
<string name="settings_magenta_theme">Maĝenta</string>
<string name="settings_violet_theme">Viola</string>
<string name="settings_blue_theme">Blua</string>
<string name="enter_card_id">Entajpu la identigilon aŭ tekston sur via karto</string>
<string name="card_id_must_not_be_empty">Identigilo devas ne esti malplena</string>
<string name="add_a_card_in_a_different_way">Aldoni karton alimaniere</string>

View File

@@ -12,13 +12,13 @@
<string name="exportFailed">No se pudo exportar</string>
<string name="noBarcode">Sin código de barra</string>
<string name="edit">Editar</string>
<string name="noGiftCards">Pulsá el botón + para agregar una tarjeta de regalo, o importá una desde el menú</string>
<string name="noGiftCardsGroup">Crea tarjetas de regalo, y asignales un grupo</string>
<string name="noGiftCards">Pulsá el botón + para agregar una tarjeta de regalo, o importá una desde el menú.</string>
<string name="noGiftCardsGroup">Crea tarjetas de regalo, y asignales un grupo.</string>
<string name="note">Nota</string>
<string name="unstar">Borrar de favoritos</string>
<string name="importExport">Importar/Exportar</string>
<string name="exportName">Exportar</string>
<string name="importExportHelp">Crear una copia de seguridad de sus datos, permite moverlos hacia otro dispositivo</string>
<string name="importExportHelp">Crear una copia de seguridad de sus datos, permite moverlos hacia otro dispositivo.</string>
<string name="importing">Importando…</string>
<string name="exporting">Exportando…</string>
<string name="save">Guardar</string>
@@ -44,12 +44,12 @@
<plurals name="deleteCardsTitle">
<item quantity="one">Borrar <xliff:g>%d</xliff:g> tajeta</item>
<item quantity="many">Borrar <xliff:g>%d</xliff:g> tarjetas</item>
<item quantity="other"/>
<item quantity="other"></item>
</plurals>
<plurals name="deleteCardsConfirmation">
<item quantity="one">¿Borrar esta<xliff:g>%d</xliff:g> tarjeta de forma permanente\?</item>
<item quantity="many">Borrar estas <xliff:g>%d</xliff:g> tarjetas de forma permanente\?</item>
<item quantity="other"/>
<item quantity="other"></item>
</plurals>
<string name="failedOpeningFileManager">Primero instale un administrador de archivos.</string>
<string name="intent_import_card_from_url_share_multiple_text">Quiero compartirte algunas tarjetas</string>
@@ -58,15 +58,18 @@
<string name="about_title_fmt">Acerca de <xliff:g id="app_name">%s</xliff:g></string>
<string name="editBarcode">Editar código de barras</string>
<string name="removeImage">Remover imágen</string>
<string name="settings_portrait_orientation">Vertical</string>
<string name="takePhoto">Tomar una foto</string>
<string name="cameraPermissionDeniedTitle">No se pudo acceder a la cámara</string>
<string name="wrongValueForBarcodeType">El valor no es válido para el tipo de código de barras seleccionado</string>
<string name="expiryDate">Fecha de vencimiento</string>
<string name="importStocard">Importar desde Stocard</string>
<string name="currency">Moneda</string>
<string name="group_edit">Editar grupo</string>
<string name="debug_version_fmt">Versión: <xliff:g id="version">%s</xliff:g></string>
<string name="backImageDescription">Imágen dorsal</string>
<string name="noCameraPermissionDirectToSystemSetting">Para escanear códigos de barra, Catima necesitará acceso a la cámara. Presione aquí para cambiar la configuración de sus permisos.</string>
<string name="settings_lock_on_opening_orientation">Bloquear a la orientación utilizada al abrir la tarjeta</string>
<string name="app_loyalty_card_keychain">Cartera para Tarjetas de Fidelización</string>
<string name="importOptionFilesystemTitle">Importar desde su sistema de archivos</string>
<string name="leaveWithoutSaveTitle">Salir</string>
@@ -78,10 +81,11 @@
<string name="settings_dark_theme">Oscuro</string>
<string name="importFidme">Importar desde FidMe</string>
<string name="settings_disable_lockscreen_while_viewing_card_summary">Deshabilita el bloqueo de pantalla mientras se ve una tarjeta</string>
<string name="exportOptionExplanation">Los datos serán escritos a una ubicación de tu elección</string>
<string name="exportOptionExplanation">Los datos serán escritos a una ubicación de tu elección.</string>
<string name="app_copyright_old">Basado en Loyalty Card Keychain
\ncopyright © 20162020 Branden Archer</string>
<string name="importVoucherVaultMessage">Seleccione su exportado desde Voucher Vault para importarlo.\nCréelo presionando la opción Exportar en Voucher Vault.</string>
<string name="importVoucherVaultMessage">Seleccione su <i>vouchervault.json</i> exportado desde Voucher Vault para importarlo.
\nPrimero créelo presionando la opción Exportar en Voucher Vault.</string>
<string name="chooseImportType">Importar datos desde</string>
<string name="frontImageDescription">Imágen frontal</string>
<string name="settings_system_theme">Sistema</string>
@@ -99,10 +103,11 @@
<string name="settings_keep_screen_on">Mantener la pantalla encendida</string>
<string name="setBarcodeId">Establecer valor del código de barras</string>
<string name="importCatima">Importar desde Catima</string>
<string name="settings_follow_system_orientation">Seguir el sistema</string>
<string name="intent_import_card_from_url_share_text">Quiero compartirte una tarjeta</string>
<string name="addFromImage">Seleccione una imágen desde la galería</string>
<string name="app_copyright_short">Copyright © Sylvia van Os y colaboradores</string>
<string name="importOptionFilesystemExplanation">Elija un archivo desde su sistema de archivos</string>
<string name="importOptionFilesystemExplanation">Elija un archivo desde su sistema de archivos.</string>
<string name="exportSuccessful">Datos exportados</string>
<string name="settings_allow_content_provider_read_summary">Las aplicaciones aún tendrán que pedir permiso para obtener acceso</string>
<string name="editGroup">Edición de grupo: <xliff:g>%s</xliff:g></string>
@@ -111,8 +116,11 @@
<string name="about">Acerca de</string>
<string name="sameAsCardId">Igual que el código</string>
<string name="importOptionFilesystemButton">Desde el sistema de archivos</string>
<string name="settings_landscape_orientation">Horizontal</string>
<string name="privacy_policy">Política de Privacidad</string>
<string name="enter_group_name">Ingrese el nombre del grupo</string>
<string name="importStocardMessage">Seleccione su <i>***.zip</i> exportado desde Stocard para importarlo.
\nObténgalo mandando un correo electrónico a support@stocardapp.com preguntando por una copia de tus datos.</string>
<string name="addManually">Ingresar el código de barras manualmente</string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Copyright © 2019<xliff:g>%d</xliff:g> Sylvia van Os y colaboradores</string>
<string name="importVoucherVault">Importar desde Voucher Vault</string>
@@ -123,7 +131,8 @@
<string name="balance">Balance</string>
<string name="cameraPermissionRequired">Se necesita permiso para acceder a la cámara para realizar esta acción…</string>
<string name="settings_allow_content_provider_read_title">Permitir que otras aplicaciones accedan a mis datos</string>
<string name="importLoyaltyCardKeychainMessage">Seleccione su exportado desde Loyalty Card Keychain para importarlo.\nCréelo desde el menu Importar/Exportar de Loyalty Card Keychain al presionar la opción Exportar.</string>
<string name="importLoyaltyCardKeychainMessage">Seleccione su <i>LoyaltyCardKeychain.csv</i> exportado desde Loyalty Card Keychain para importarlo.
\nPrimero créelo desde el menu Importar/Exportar de Loyalty Card Keychain al presionar la opción Exportar.</string>
<string name="settings_light_theme">Claro</string>
<string name="moveDown">Mover hacia abajo</string>
<string name="importLoyaltyCardKeychain">Importar desde Loyalty Card Keychain</string>
@@ -141,27 +150,30 @@
<string name="settings">Configuración</string>
<string name="selectBarcodeTitle">Seleccione el código de barras</string>
<string name="importSuccessful">Datos importados</string>
<string name="app_libraries">Librerías externas: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_resources">Recursos externos: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="settings_card_orientation">Orientación del código de barras</string>
<string name="app_libraries">Librerías externas libres: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_resources">Recursos externos libres: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_name">Catima</string>
<string name="accept">Aceptar</string>
<plurals name="groupCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> tarjeta</item>
<item quantity="many"><xliff:g>%d</xliff:g> tarjetas</item>
<item quantity="other"/>
<item quantity="other"></item>
</plurals>
<plurals name="selectedCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> seleccionado</item>
<item quantity="many"><xliff:g>%d</xliff:g> seleccionados</item>
<item quantity="other"/>
<item quantity="other"></item>
</plurals>
<string name="importCatimaMessage">Seleccione su exportado desde Catima para importarlo.\nCréelo desde el menu para Importar/Exportar de otra aplicación de Catima presionando la opción Exportar.</string>
<string name="importCatimaMessage">Seleccione su <i>catima.zip</i> exportado desde Catima para importarlo.
\nPrimero créelo desde el menu para Importar/Exportar de otra aplicación de Catima presionando la opción Exportar.</string>
<plurals name="balancePoints">
<item quantity="one"><xliff:g>%s</xliff:g> punto</item>
<item quantity="many"><xliff:g>%s</xliff:g> puntos</item>
<item quantity="other"/>
<item quantity="other"></item>
</plurals>
<string name="importFidmeMessage">Seleccione su exportado de FidMe para importarlo, y a continuación seleccione manualmente los tipos de código de barras.\nCréelo desde su perfil de FidMe al elegir la opción Protección de Datos y presionando Extraer mis datos.</string>
<string name="importFidmeMessage">Seleccione su <i>fidme-export-request-xxxxxx.zip</i> exportado de FidMe para importarlo, y a continuación seleccione manualmente los tipos de código de barras.
\nPrimero créelo desde su perfil de FidMe al elegir la opción Protección de Datos y presionando Extraer mis datos.</string>
<string name="updateBarcodeQuestionTitle">¿Actualizar el valor del código de barras\?</string>
<string name="settings_keep_screen_on_summary">Deshabilita el tiempo de espera de la pantalla mientras se ve una tarjeta</string>
<string name="thumbnailDescription">Miniatura</string>
@@ -169,7 +181,11 @@
<string name="updateBarcodeQuestionText">Has cambiado el ID. ¿Quieres actualizar también el código de barras para usar el mismo valor?</string>
<string name="settings_locale">Idioma</string>
<string name="settings_system_locale">Sistema</string>
<string name="settings_catima_theme">Catima</string>
<string name="settings_pink_theme">Rosa</string>
<string name="exportPassword">Configura una contraseña para proteger tu exportación (opcional)</string>
<string name="settings_sky_blue_theme">Celeste</string>
<string name="settings_green_theme">Verde</string>
<string name="exportPasswordHint">Ingresar contraseña</string>
<string name="setIcon">Establecer miniatura</string>
<string name="showMoreInfo">Mostrar información</string>
@@ -180,21 +196,25 @@
<string name="settings_oled_dark_summary">Reduce uso de batería en pantallas OLED</string>
<string name="settings_oled_dark">Fondo negro puro para tema oscuro</string>
<string name="selectColor">Seleccionar color</string>
<string name="settings_theme_color">Color del tema</string>
<string name="settings_magenta_theme">Magenta</string>
<string name="settings_violet_theme">Violeta</string>
<string name="settings_brown_theme">Marrón</string>
<string name="sort">Ordenar</string>
<string name="settings_blue_theme">Azul</string>
<string name="app_contributors">Hecho posible por: <xliff:g id="app_contributors">%s</xliff:g></string>
<string name="barcodeLongPressMessage">Solo se puede abrir imágenes en la aplicación de galería</string>
<string name="settings_follow_sensor_orientation">Siempre rotar (ignora configuración del sistema)</string>
<string name="yes">Si</string>
<string name="no">No</string>
<string name="passwordRequired">Ingresa la contraseña</string>
<string name="failedGeneratingShareURL">No se pudo generar URL compartible</string>
<string name="passwordRequired">Por favor ingresa la contraseña</string>
<string name="failedGeneratingShareURL">No se pudo generar URL compartible. Por favor reporte esto.</string>
<string name="sort_by_name">Nombre</string>
<string name="sort_by">Ordenar por</string>
<string name="reverse">en orden inverso</string>
<string name="sort_by_most_recently_used">Más recientemente usado</string>
<string name="sort_by_most_recently_used">Más Recientemente Usado</string>
<string name="settings_use_volume_keys_navigation">Cambiar tarjetas usando los botones de volumen</string>
<string name="sort_by_valid_from">Válido Desde</string>
<string name="sort_by_expiry">Vencimiento</string>
<string name="settings_use_volume_keys_navigation_summary">Usá los botones de volumen para cambiar la tarjeta que se muestra</string>
<string name="cardWithNumberAndLocale">Tarjeta <xliff:g>%d</xliff:g> (<xliff:g>%s</xliff:g>)</string>
<string name="cardWithNumber">Tarjeta <xliff:g>%d</xliff:g></string>
</resources>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" xmlns:tools="http://schemas.android.com/tools">
<string name="action_add">Añadir</string>
<string name="noGiftCards">Haz clic en el botón + para añadir una tarjeta, o importa desde el menú ⋮</string>
<string name="noGiftCards">Haz clic en el botón + para añadir una tarjeta, o importa desde el menú ⋮.</string>
<string name="storeName">Nombre</string>
<string name="note">Nota</string>
<string name="cardId">ID de tarjeta</string>
@@ -20,7 +20,7 @@
<string name="noCardExistsError">No se ha podido encontrar esa tarjeta</string>
<string name="importExport">Importar/Exportar</string>
<string name="exportName">Exportar</string>
<string name="importExportHelp">Respaldar tus datos permite trasladarlos a otro dispositivo</string>
<string name="importExportHelp">Respaldar tus datos permite trasladarlos a otro dispositivo.</string>
<string name="importSuccessfulTitle">Importado</string>
<string name="importFailedTitle">Falló la importación</string>
<string name="importFailed">No se ha podido realizar la importación</string>
@@ -30,7 +30,7 @@
<string name="importing">Importando…</string>
<string name="exporting">Exportando…</string>
<string name="importOptionFilesystemTitle">Importar desde el sistema de archivos</string>
<string name="importOptionFilesystemExplanation">Elegir un archivo concreto del sistema de archivos</string>
<string name="importOptionFilesystemExplanation">Elegir un archivo concreto del sistema de archivos.</string>
<string name="importOptionFilesystemButton">Desde el sistema de archivos</string>
<string name="about">Información</string>
<string name="app_license">Programa libre con «copyleft», disponible en virtud de la licencia GPLv3+</string>
@@ -47,13 +47,13 @@
<string name="settings_theme">Tema</string>
<string name="app_copyright_old">Basado en Loyalty Card Keychain
\nderechos de autor © 2016-2020 de Branden Archer</string>
<string name="exportOptionExplanation">Los datos se guardarán en la ubicación que elija</string>
<string name="exportOptionExplanation">Los datos se guardarán en la ubicación que elija.</string>
<string name="failedParsingImportUriError">No se pudo procesar el URI de importación</string>
<string name="share">Compartir</string>
<string name="barcodeType">Tipo de código de barras</string>
<string name="noMatchingGiftCards">Sin resultados. Intente cambiar su búsqueda.</string>
<string name="action_search">Buscar</string>
<string name="noGroups">Pulse en el botón + para añadir grupos de categorización</string>
<string name="noGroups">Pulse en el botón + para añadir grupos de categorización.</string>
<string name="starImage">Estrella favorita</string>
<string name="thumbnailDescription">Miniatura</string>
<string name="selectBarcodeTitle">Seleccionar el código de barras</string>
@@ -67,7 +67,7 @@
<string name="leaveWithoutSaveTitle">Salir</string>
<string name="moveDown">Bajar</string>
<string name="moveUp">Subir</string>
<string name="failedOpeningFileManager">No se puedo abrir un gestor de archivos</string>
<string name="failedOpeningFileManager">Instale un gestor de archivos primero.</string>
<string name="deleteConfirmationGroup">¿Quiere eliminar el grupo\?</string>
<string name="all">Todo</string>
<string name="star">Añadir a favoritos</string>
@@ -86,14 +86,20 @@
<string name="expiryStateSentenceExpired">Expirado: <xliff:g>%s</xliff:g></string>
<string name="expiryStateSentence">Expira: <xliff:g>%s</xliff:g></string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Derechos de autor © 2019-<xliff:g>%d</xliff:g> Sylvia van Os y colaboradores</string>
<string name="app_resources">Recursos de terceros: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Bibliotecas de terceros: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="importCatimaMessage">Seleccione su exportado desde Catima para importarlo.\nCréalo primero desde el menú Importar/Exportar de otra app de Catima al presionar Exportar desde allí.</string>
<string name="importFidmeMessage">Seleccione su exportado desde FidMe para importar, y luego escoja los tipos de códigos de barras manualmente.\nCréalo desde tu perfil de FidMe eligiendo Protección de datos y pulsa Extraer mis datos.</string>
<string name="importLoyaltyCardKeychainMessage">Seleccione su exportado desde Loyalty Card Keychain para importarlo.\nCréalo desde el menú Importar/Exportar en Loyalty Card Keychain pulsando Exportar desde allí.</string>
<string name="importVoucherVaultMessage">Seleccione su exportado desde Voucher Vault para importarlo.\nCréalo pulsando Exportar en Voucher Vault.</string>
<string name="failedGeneratingShareURL">No se ha podido generar una URL compartible</string>
<string name="passwordRequired">Introduzca la contraseña</string>
<string name="app_resources">Recursos de terceros libres: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Bibliotecas de terceros libres: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="importCatimaMessage">Seleccione su <i>catima.zip</i> exportado desde Catima para importarlo.
\nCréalo primero desde el menú Importar/Exportar de otra app de Catima al presionar Exportar desde allí.</string>
<string name="importFidmeMessage">Seleccione su <i>fidme-export-request-xxxxxx.zip</i> exportado desde FidMe para importar, y luego escoja los tipos de códigos de barras manualmente.
\nCréalo primero desde tu perfil de FidMe eligiendo Protección de datos y pulsa Extraer mis datos.</string>
<string name="importLoyaltyCardKeychainMessage">Seleccione su <i>LoyaltyCardKeychain.csv</i> exportado desde Loyalty Card Keychain para importarlo.
\nCréalo primero desde el menú Importar/Exportar en Loyalty Card Keychain pulsando Exportar desde allí.</string>
<string name="importStocardMessage">Seleccione su exportación <i>*.zip</i> de Stocard para importarla.
\nConsígalo enviando un correo electrónico a support@stocardapp.com solicitando una exportación de sus datos.</string>
<string name="importVoucherVaultMessage">Seleccione su <i>vouchervault.json</i> exportado desde Voucher Vault para importarlo.
\nCréalo pulsando primero Exportar en Voucher Vault.</string>
<string name="failedGeneratingShareURL">No se ha podido generar una URL compartible. Por favor, informe de ello.</string>
<string name="passwordRequired">Por favor, introduzca la contraseña</string>
<string name="updateBarcodeQuestionText">Ha cambiado el código. ¿Desea actualizar también el código de barras para usar el mismo valor\?</string>
<string name="intent_import_card_from_url_share_multiple_text">Quiero compartirte algunas tarjetas</string>
<string name="setBackImage">Establecer imagen anversa</string>
@@ -114,6 +120,7 @@
<string name="sameAsCardId">Igual que el código</string>
<string name="barcodeId">Valor de código de barra</string>
<string name="importVoucherVault">Importar desde Voucher Vault</string>
<string name="importStocard">Importar desde Stocard</string>
<string name="importLoyaltyCardKeychain">Importar desde Loyalty Card Keychain</string>
<string name="importFidme">Importar desde FidMe</string>
<string name="importCatima">Importar desde Catima</string>
@@ -147,18 +154,32 @@
<item quantity="other">Borrar <xliff:g>%d</xliff:g> tarjetas</item>
</plurals>
<string name="app_contributors">Hecho posible por: <xliff:g id="app_contributors">%s</xliff:g></string>
<string name="settings_brown_theme">Marrón</string>
<string name="settings_green_theme">Verde</string>
<string name="settings_sky_blue_theme">Azul cielo</string>
<string name="settings_blue_theme">Azul</string>
<string name="settings_violet_theme">Violeta</string>
<string name="settings_magenta_theme">Magenta</string>
<string name="settings_pink_theme">Rosa</string>
<string name="settings_theme_color">Color del tema</string>
<string name="settings_catima_theme">Catima</string>
<string name="settings_system_locale">Sistema</string>
<string name="settings_locale">Idioma</string>
<string name="noGroupCards">Este grupo está vacío</string>
<string name="settings_landscape_orientation">Horizontal</string>
<plurals name="balancePoints">
<item quantity="one"><xliff:g>%s</xliff:g> punto</item>
<item quantity="many"><xliff:g>%s</xliff:g> puntos</item>
<item quantity="other"><xliff:g>%s</xliff:g> puntos</item>
</plurals>
<string name="barcodeImageDescriptionWithType">Imagen <xliff:g>%s</xliff:g> código de barras</string>
<string name="settings_card_orientation">Orientación de pantalla</string>
<string name="settings_portrait_orientation">Formato vertical</string>
<string name="group_edit">Editar grupo</string>
<string name="group_updated">Grupo actualizado</string>
<string name="noGiftCardsGroup">Crea algunas tarjetas y luego asígnelas al grupo aquí</string>
<string name="noGiftCardsGroup">Crea algunas tarjetas y luego asígnelas al grupo aquí.</string>
<string name="settings_follow_system_orientation">Segue el sistema</string>
<string name="settings_lock_on_opening_orientation">Bloqueo a la orientación utilizada al abrir la tarjeta</string>
<string name="sort_by_most_recently_used">Lo más usado recientemente</string>
<string name="sort_by_expiry">Caducidad</string>
<string name="version_history">Historial de versiones</string>
@@ -197,7 +218,7 @@
<string name="archived">Tarjeta archivada</string>
<string name="unarchived">Tarjeta desarchivada</string>
<string name="exportPassword">Establezca una contraseña para proteger su exportación (opcional)</string>
<string name="failedLaunchingPhotoPicker">No se encontró una aplicación de galería compatible</string>
<string name="failedLaunchingPhotoPicker">No se ha encontró una aplicación de galería compatible</string>
<plurals name="groupCardCountWithArchived">
<item quantity="one"><xliff:g>%1$d</xliff:g> tarjeta (archivada)<xliff:g id="archivedCount">%2$d</xliff:g></item>
<item quantity="many"><xliff:g>%1$d</xliff:g> tarjetas (archivadas)<xliff:g id="archivedCount">%2$d</xliff:g></item>
@@ -223,7 +244,7 @@
<string name="height">Alto</string>
<string name="switchToFrontImage">Cambiar a imagen frontal</string>
<string name="openFrontImageInGalleryApp">Abrir imagen frontal en la aplicación de la galería</string>
<string name="openBackImageInGalleryApp">Abrir imagen trasera en la aplicación de visor de imagen</string>
<string name="openBackImageInGalleryApp">Abrir imagen trasera en la aplicación de la galería</string>
<string name="setBarcodeHeight">Ajustar la altura del código de barras</string>
<string name="donate">Donar</string>
<string name="switchToBarcode">Cambiar a código de barras</string>
@@ -257,9 +278,10 @@
<string name="addWithoutBarcode">Añadir una tarjeta sin código de barras</string>
<string name="field_must_not_be_empty">Este campo no debe estar vacío</string>
<string name="app_name">Catima</string>
<string name="settings_follow_sensor_orientation">Girar siempre (ignora la configuración del sistema)</string>
<string name="continue_">Continuar</string>
<string name="add_manually_warning_title">Se recomienda escanear</string>
<string name="add_manually_warning_message">En algunas tarjetas, el valor del código de barras difiere del número escrito en la tarjeta. Por este motivo, introducir manualmente puede que no siempre funcione. Se recomienda analizar el código de barras con su cámara en su lugar. ¿Aún desea continuar?</string>
<string name="add_manually_warning_message">En algunas tiendas, el valor del código de barras difiere del número escrito en la tarjeta. Por este motivo, es posible que la introducción manual del código de barras no siempre funcione. Se recomienda encarecidamente escanear el código de barras con la cámara. ¿Aún desea continuar?</string>
<string name="spend">Gastar</string>
<string name="receive">Recibió</string>
<string name="amountParsingFailed">Importe incorrecto</string>
@@ -286,23 +308,12 @@
<string name="settings_column_count_5">5</string>
<string name="settings_column_count_6">6</string>
<string name="settings_column_count_7">7</string>
<string name="generic_error_please_retry">Algo salió mal</string>
<string name="generic_error_please_retry">Lo sentimos, algo salió mal, por favor inténtelo de nuevo...</string>
<string name="unsupportedFile">Este archivo no es compatible</string>
<string name="addFromPkpass">Seleccione un archivo Passbook (.pkpass / .pkpasses)</string>
<string name="addFromPkpass">Seleccione un archivo Passbook (.pkpass)</string>
<string name="sort_by_valid_from">Válido desde</string>
<string name="setBarcodeWidth">Establecer el ancho del código de barras</string>
<string name="width">Ancho</string>
<string name="card_list_widget_name">Lista de tarjetas</string>
<string name="card_list_widget_empty">Después de añadir algunas tarjetas de fidelidad en Catima, aparecerán aquí. Si tienes tarjetas, asegúrate de que no estén archivadas.</string>
<string name="cardWithNumber">Tarjeta <xliff:g>%d</xliff:g></string>
<string name="cardWithNumberAndLocale">Tarjeta <xliff:g>%d</xliff:g> (<xliff:g>%s</xliff:g>)</string>
<string name="pleaseDoNotRotateTheDevice">Por favor, no rote el dispositivo, ya que esto cancelará la acción</string>
<string name="acra_catima_has_crashed">Lo sentimos, pero <xliff:g id="app_name">%s</xliff:g> ha fallado. Por favor, ayúdenos a resolver esta incidencia enviándonos un reporte del error.</string>
<string name="acra_explain_crash">Si es posible, por favor añada más detalles sobre lo que estaba haciendo aquí:</string>
<string name="acra_crash_email_subject">Reporte del fallo <xliff:g id="app_name">%s</xliff:g></string>
<string name="pref_enable_acra">Solicitar envío de reportes de fallos</string>
<string name="pref_enable_acra_summary">Cuando está activado, se le pedirá que informe sobre un fallo cuando ocurra. Los informes de fallo nunca se envían automáticamente.</string>
<string name="copy_value">Copia valor</string>
<string name="copied_to_clipboard">Copiado al portapapeles</string>
<string name="nothing_to_copy">Ningún valor encontrado</string>
</resources>

Some files were not shown because too many files have changed in this diff Show More