Compare commits

..

1 Commits

Author SHA1 Message Date
Sylvia van Os
70cdb18c51 WIP 2023-03-17 17:18:43 +01:00
1844 changed files with 10231 additions and 26901 deletions

View File

@@ -1,30 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "gradle"
directory: "/"
registries:
- google
- gradlePluginPortal
- jitpack
- mavenCentral
- package-ecosystem: "gradle" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
# Workaround for https://github.com/dependabot/dependabot-core/issues/6888
registries:
google:
type: maven-repository
url: "https://dl.google.com/dl/android/maven2/"
gradlePluginPortal:
type: maven-repository
url: "https://plugins.gradle.org/m2/"
jitpack:
type: maven-repository
url: "https://jitpack.io/"
mavenCentral:
type: maven-repository
url: "https://repo1.maven.org/maven2/"

View File

@@ -1,6 +1,6 @@
name: Android CI
on:
workflow_dispatch:
push:
branches:
- main
@@ -9,66 +9,33 @@ on:
pull_request:
branches:
- main
permissions:
actions: none
checks: none
contents: read
deployments: none
discussions: none
id-token: none
issues: none
packages: none
pages: none
pull-requests: none
repository-projects: none
security-events: none
statuses: none
env:
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
flavor: [Foss, Gplay]
steps:
- uses: actions/checkout@v4.2.2
- 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 17
run: |
sudo apt-get update
sudo apt-get install -y openjdk-17-jdk-headless
sudo update-alternatives --auto java
- name: Build
run: ./gradlew assemble${{ matrix.flavor }}Release
- name: Check lint
run: ./gradlew lint${{ matrix.flavor }}Release
- name: Run unit tests
run: timeout 5m ./gradlew test${{ matrix.flavor }}ReleaseUnitTest || { ./gradlew --stop && timeout 5m ./gradlew test${{ matrix.flavor }}ReleaseUnitTest; }
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Run instrumented tests (API 21)
uses: ReactiveCircus/android-emulator-runner@v2
with:
api-level: 21
arch: x86_64
script: ./gradlew connected${{ matrix.flavor }}DebugAndroidTest
- name: Run instrumented tests (API 34)
uses: ReactiveCircus/android-emulator-runner@v2
with:
api-level: 34
arch: x86_64
script: ./gradlew connected${{ matrix.flavor }}DebugAndroidTest
- name: SpotBugs
run: ./gradlew spotbugs${{ matrix.flavor }}Release
- name: Archive test results
if: always()
uses: actions/upload-artifact@v4.5.0
with:
name: test-results-flavor${{ matrix.flavor }}
path: app/build/reports
- uses: actions/checkout@v2
- 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/wrapper-validation-action@v1
- name: set up JDK 11
uses: actions/setup-java@v2
with:
distribution: 'adopt'
java-version: '11'
- name: Build
run: ./gradlew assembleRelease
- name: Check lint
run: ./gradlew lintRelease
- name: Run unit tests
run: ./gradlew testReleaseUnitTest || ./gradlew testReleaseUnitTest
- name: SpotBugs
run: ./gradlew spotbugsRelease
- name: Archive test results
if: always()
uses: actions/upload-artifact@v2
with:
name: test-results
path: app/build/reports

View File

@@ -0,0 +1,24 @@
name: 'Close issues and PRs needing info for too long'
on:
schedule:
- cron: '30 1 * * *'
permissions:
issues: write
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v4
with:
days-before-stale: -1
days-before-close: 90
close-issue-message: 'This issue is missing necessary information and cannot be worked on in its current state. It has therefore been closed to keep the issue tracker clean. If you have more information, feel free to reopen it.'
close-pr-message: 'This PR is missing necessary information and cannot be merged in its current state. It has therefore been closed to keep the issue tracker clean. If you have more information, feel free to reopen it.'
only-labels: 'state: needs info'
stale-issue-label: 'state: needs info'
stale-pr-label: 'state: needs info'
remove-stale-when-updated: false
enable-statistics: true

View File

@@ -0,0 +1,34 @@
name: Compress Images on Push to Main
on:
push:
branches:
- main
paths:
- '**.jpg'
- '**.jpeg'
- '**.png'
- '**.webp'
jobs:
build:
# Only run on Pull Requests within the same repository, and not from forks.
if: github.event.pull_request.head.repo.full_name == github.repository
name: calibreapp/image-actions
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v2
- name: Compress Images
id: calibre
uses: calibreapp/image-actions@1.1.0
with:
githubToken: ${{ secrets.GITHUB_TOKEN }}
ignorePaths: 'app/src/test'
compressOnly: true
- name: Create New Pull Request If Needed
if: steps.calibre.outputs.markdown != ''
uses: peter-evans/create-pull-request@v3
with:
title: Compressed Images
branch-suffix: timestamp
commit-message: Compressed Images
body: ${{ steps.calibre.outputs.markdown }}

View File

@@ -1,25 +1,9 @@
name: Convert CHANGELOG to Fastlane
on:
workflow_dispatch:
push:
branches:
- 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
@@ -27,15 +11,15 @@ jobs:
steps:
- name: Checkout repo
id: checkout
uses: actions/checkout@v4.2.2
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v5.3.0
uses: actions/setup-python@v2
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@v7.0.5
uses: peter-evans/create-pull-request@v3
with:
title: "Update Fastlane changelogs"
commit-message: "Update Fastlane changelogs"

View File

@@ -1,22 +1,8 @@
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
@@ -25,15 +11,14 @@ jobs:
steps:
- name: Checkout repo
id: checkout
uses: actions/checkout@v4.2.2
uses: actions/checkout@v2
- name: Update contributors
id: update_contributors
uses: TheLastProject/contributors-to-file-action@v3.2.0
uses: TheLastProject/contributors-to-file-action@v2
with:
file_in_repo: app/src/main/res/raw/contributors.txt
min_commit_count: 5
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7.0.5
uses: peter-evans/create-pull-request@v3
with:
title: "Update contributors"
commit-message: "Update contributors"

View File

@@ -1,46 +0,0 @@
name: Generate feature graphic
on:
workflow_dispatch:
push:
branches:
- main
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@v4.2.2
- name: Install requirements
run: |
sudo apt-get update
sudo apt-get install optipng mat2
# Install 200 weight versions of relevant Noto (to use for languages not supported by Lexend Deca)
sudo apt-get install fonts-noto-extra fonts-noto-cjk-extra
# Custom fonts
mkdir "$HOME/.fonts"
find .scripts/generate_feature_graphic/fonts -name '*.ttf' -exec cp {} "$HOME/.fonts" \;
fc-cache
- 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@v7.0.5
with:
title: "Update feature graphic"
commit-message: "Update feature graphic"
branch-suffix: timestamp

View File

@@ -1,33 +0,0 @@
name: Gradle update
on:
workflow_dispatch:
schedule:
- cron: '3 6 * * *'
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:
gradle-update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- uses: obfusk/gradle-update-action@v3.0.0
id: gradle-update
- uses: gradle/actions/wrapper-validation@v4
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7.0.5
with:
title: "Update Gradle to ${{ steps.gradle-update.outputs.version }}"
commit-message: "Update Gradle to ${{ steps.gradle-update.outputs.version }}"
branch-suffix: timestamp

View File

@@ -0,0 +1,10 @@
name: "Validate Gradle Wrapper"
on: [push, pull_request]
jobs:
validation:
name: "Validation"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1

View File

@@ -1,38 +0,0 @@
name: Update locales
on:
workflow_dispatch:
push:
branches:
- main
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@v4.2.2
- 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@v7.0.5
with:
title: "Update locales"
commit-message: "Update locales"
branch-suffix: timestamp

29
.gitignore vendored
View File

@@ -1,30 +1,15 @@
# Android Studio generated (superseded/unused rules commented out)
*.iml
.gradle
/local.properties
#/.idea/caches
#/.idea/libraries
#/.idea/modules.xml
#/.idea/workspace.xml
#/.idea/navEditor.xml
#/.idea/assetWizardSettings.xml
local.properties
.idea/
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
#local.properties
# Android extras
/app/*.log
/app/build
/app/release
/.idea
build/
captures/
**/release
**/debug
app/*.log
# Bundle
/.bundle/
/vendor/bundle
/lib/bundler/man/
# Catima-specific
SHA256SUMS

View File

@@ -1,44 +0,0 @@
#!/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

@@ -1,15 +0,0 @@
<svg width="1024" height="500" viewBox="0 0 1024 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="1024" height="500" fill="#223355"/>
<text fill="white" xml:space="preserve" style="" font-family="Yesteryear" font-size="150" letter-spacing="0em"><tspan x="470.082" y="285.511">Catima
</tspan></text>
<path d="M381.046 147.001L236.3 211.446L276.524 301.79L421.27 237.345L381.046 147.001Z" fill="#F0F0F0" stroke="#C80000" stroke-width="2"/>
<path d="M402.077 219.13L240.07 147L191.984 255.004L353.99 327.135L402.077 219.13Z" fill="#F0F0F0" stroke="#C80000" stroke-width="2"/>
<path d="M437.17 236.241L251.831 183.096L220.071 293.855L405.41 347L437.17 236.241Z" fill="#C80000" stroke="#C80000" stroke-width="6" stroke-linejoin="round"/>
<path d="M412.879 178.633H220.071V293.855H412.879V178.633Z" fill="#FF0000" stroke="#FF0000" stroke-width="6" stroke-linejoin="round"/>
<path d="M221.482 296.217C238.316 296.217 251.963 269.366 251.963 236.244C251.963 203.121 238.316 176.27 221.482 176.27C204.647 176.27 191 203.121 191 236.244C191 269.366 204.647 296.217 221.482 296.217Z" fill="#FF0000" stroke="#FF0000" stroke-width="3.44232" stroke-linejoin="round"/>
<path d="M307.256 250.444C307.256 253.187 306.289 255.842 304.526 257.944C302.763 260.045 300.316 261.458 297.614 261.934C294.913 262.41 292.13 261.92 289.755 260.548C287.379 259.177 285.563 257.012 284.625 254.435" stroke="#F0F0F0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M330.301 254.298C329.363 256.875 327.547 259.04 325.171 260.411C322.796 261.783 320.013 262.273 317.312 261.797C314.61 261.321 312.163 259.908 310.4 257.807C308.637 255.706 307.671 253.05 307.671 250.307" stroke="#F0F0F0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M248.345 225.937L266.818 207.465L285.29 225.937" stroke="#F0F0F0" stroke-width="2"/>
<path d="M329.625 225.937L348.098 207.465L366.571 225.937" stroke="#F0F0F0" stroke-width="2"/>
<text fill="white" xml:space="preserve" style="" font-family="Lexend Deca" font-size="35" font-weight="200" letter-spacing="0em"><tspan x="466" y="340">Loyalty Card Wallet</tspan></text>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1,93 +0,0 @@
Copyright 2018 The Lexend Project Authors (https://github.com/googlefonts/lexend), with Reserved Font Name “RevReading Lexend”.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -1,94 +0,0 @@
Copyright (c) 2011 by Brian J. Bonislawsky DBA Astigmatic (AOETI)
(astigma@astigmatic.com), with Reserved Font Names "Yesteryear"
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -1,60 +0,0 @@
#!/bin/bash
set -euo pipefail
script_location="$(dirname "$(readlink -f "$0")")"
for lang in "$script_location/../../fastlane/metadata/android/"*; do
# Skip languages without title.txt
if [ ! -f "$lang/title.txt" ]; then
continue
fi
pushd "$lang"
# Place temporary copy for editing if needed
cp "$script_location/featureGraphic.svg" featureGraphic.svg
if grep -q — title.txt; then
# Try splitting title.txt on — (em dash)
IFS='—' read -r appname subtext < title.txt
elif grep -q title.txt; then
# No result, try splitting title.txt on (en dash)
IFS='' read -r appname subtext < title.txt
elif grep -q - title.txt; then
# No result, try splitting on - (dash)
IFS='-' read -r appname subtext < title.txt
else
# No result, use the full title as app name and default subtext
appname=$(< title.txt)
subtext="Loyalty Card Wallet"
fi
export appname=${appname%% }
export subtext=${subtext## }
# If the appname isn't Catima or there is subtext, change the .svg accordingly
if [ "$appname" != "Catima" ] || [ -n "$subtext" ]; then
perl -pi -e 's/Catima/$ENV{appname}/' featureGraphic.svg
perl -pi -e 's/Loyalty Card Wallet/$ENV{subtext}/' featureGraphic.svg
# Set correct font or font size for language if needed
# (Lexend Deca has limited support and some characters are big)
# We specifically need the Serif version because of the 200 weight
case "$(basename "$lang")" in
bg|el-GR|ru-RU|uk) sed -i "s/Lexend Deca/Noto Serif/" featureGraphic.svg ;;
hi-IN) sed -i -e "s/Yesteryear/Noto Sans Devanagari/" -e "s/Lexend Deca/Noto Serif Devanagari/" featureGraphic.svg ;;
ja-JP) sed -i "s/Lexend Deca/Noto Serif CJK JP/" featureGraphic.svg ;;
kn-IN) sed -i -e 's/font-size="150"/font-size="100"/' -e "s/Yesteryear/Noto Serif Kannada/" featureGraphic.svg ;;
ko) sed -i "s/Lexend Deca/Noto Serif CJK KR/" featureGraphic.svg ;;
zh-CN) sed -i "s/Lexend Deca/Noto Serif CJK SC/" featureGraphic.svg ;;
zh-TW) sed -i -e "s/Yesteryear/Noto Sans CJK TC/" -e "s/Lexend Deca/Noto Serif CJK TC/" featureGraphic.svg ;;
*) ;;
esac
fi
# Ensure images directory exists
mkdir -p images
# Generate .png
convert featureGraphic.svg images/featureGraphic.png
# Optimize .png
optipng images/featureGraphic.png
# Remove metadata (timestamps) from .png
mat2 --inplace images/featureGraphic.png
# Remove temporary .svg
rm featureGraphic.svg
popd
done

View File

@@ -1,36 +0,0 @@
#!/usr/bin/python3
import subprocess
import xml.etree.ElementTree as ET
root = ET.parse("app/src/main/res/values/settings.xml").getroot()
for e in root.findall("string-array"):
if e.get("name") == "locale_values":
locales = [x.text for x in e if x.text]
break
locales = [
# e.g. de or es-rAR (not es-AR)
loc.replace("-", "-r") if "-" in loc and loc[loc.index("-") + 1] != "r" else loc
for loc in locales
]
res = ", ".join(f'"{loc}"' for loc in locales)
sed = [
"sed",
"-i",
f"s/resourceConfigurations .*/resourceConfigurations += listOf({res})/",
"app/build.gradle.kts"
]
subprocess.run(sed, check=True)
with open("app/src/main/res/xml/locales_config.xml", "w", encoding="utf-8") as fh:
fh.write('<?xml version="1.0" encoding="utf-8"?>\n')
fh.write('<locale-config xmlns:android="http://schemas.android.com/apk/res/android">\n')
fh.write(' <locale android:name="en-US" />\n')
for loc in locales:
if loc != "en":
# e.g. de or en-AR (not es-rAR)
loc = loc.replace("-r", "-")
fh.write(f' <locale android:name="{loc}" />\n')
fh.write('</locale-config>\n')

View File

@@ -1,132 +0,0 @@
#!/usr/bin/python3
import glob
import re
from typing import Iterator, List, Tuple
import requests
MIN_PERCENT = 90
NOT_LANGS = ("night", "w600dp")
REPLACE_CODES = {
"el": "el-rGR",
"id": "in-rID",
"ro": "ro-rRO",
"zh_Hans": "zh-rCN",
"zh_Hant": "zh-rTW",
}
STATS_URL = "https://hosted.weblate.org/api/components/catima/catima/statistics/"
class Error(Exception):
pass
def get_weblate_langs() -> List[Tuple[str, int]]:
url = STATS_URL
results = []
for _ in range(16): # avoid endless loops just in case
r = requests.get(url, timeout=5)
r.raise_for_status()
data = r.json()
for lang in data["results"]:
if lang["code"] != "en":
code = REPLACE_CODES.get(lang["code"], lang["code"]).replace("_", "-r")
results.append((code, round(lang["translated_percent"])))
url = data["next"]
if not url:
return sorted(results)
if not url.split("?")[0] == STATS_URL:
raise Error(f"Unexpected next URL: {url}")
raise Error("Too many pages")
def get_dir_langs() -> List[str]:
results = []
for d in glob.glob("app/src/main/res/values-*"):
code = d.split("-", 1)[1]
if code not in NOT_LANGS:
results.append(code)
return sorted(results)
def get_xml_langs() -> List[Tuple[str, bool]]:
results = []
in_section = False
with open("app/src/main/res/values/settings.xml", encoding="utf-8") as fh:
for line in fh:
if not in_section and 'name="locale_values"' in line:
in_section = True
elif in_section:
if "string-array" in line:
break
disabled = "<!--" in line
if m := re.search(r">(.*)<", line):
if m[1] != "en":
results.append((m[1], disabled))
return sorted(results)
def update_xml_langs(langs: List[Tuple[str, bool]]) -> None:
lines: List[str] = []
in_section = False
with open("app/src/main/res/values/settings.xml", encoding="utf-8") as fh:
for line in fh:
if not in_section and 'name="locale_values"' in line:
in_section = True
elif in_section:
if "string-array" in line:
in_section = False
lines.extend(_lang_lines(langs))
else:
continue
lines.append(line)
with open("app/src/main/res/values/settings.xml", "w", encoding="utf-8") as fh:
for line in lines:
fh.write(line)
def _lang_lines(langs: List[Tuple[str, bool]]) -> Iterator[str]:
yield " <item />\n"
for lang, disabled in sorted(langs + [("en", False)]):
if disabled:
yield f" <!-- <item>{lang}</item> -->\n"
else:
yield f" <item>{lang}</item>\n"
def main() -> None:
web_langs = get_weblate_langs()
dir_langs = get_dir_langs()
xml_langs = get_xml_langs()
web_codes = set(code for code, _ in web_langs)
dir_codes = set(dir_langs)
xml_codes = set(code for code, _ in xml_langs)
if diff := web_codes - dir_codes:
print(f"WARNING: Weblate codes w/o dir: {diff}")
if diff := xml_codes - dir_codes:
print(f"WARNING: XML codes w/o dir: {diff}")
percentages = dict(web_langs)
all_langs = xml_langs[:]
# add new langs as disabled
for code in dir_codes - xml_codes:
all_langs.append((code, True))
# enable disabled langs if they are at least MIN_PERCENT translated now
updated_langs = sorted(
(code, percentages[code] < MIN_PERCENT if disabled else disabled)
for code, disabled in all_langs
)
if updated_langs != xml_langs:
print("Updating...")
update_xml_langs(updated_langs)
if __name__ == "__main__":
main()

View File

@@ -1,186 +1,43 @@
# Changelog
## v2.34.2 - 144 (2024-12-26)
- Improve archive/starred icon display
## v2.34.1 - 143 (2024-12-12)
- Fix crash when opening invalid pkpass files
## v2.34.0 - 142 (2024-12-10)
- Add Passbook (.pkpass) support
- Fix import of transparent PDF files
- Improve display of transparent thumbnails
## v2.33.0 - 141 (2024-11-19)
- Change default column on wide screens to 4
- Allow overriding column counts for portrait and landscape in settings
- Keep main screen search filter when rotating screen or opening a card
- Limit max length of note display on main screen
## v2.32.1 - 140 (2024-10-29)
- Fix text wrapping on add dialog
## v2.32.0 - 139 (2024-10-28)
- Option to navigate cards using the volume buttons
- Fix Stocard import
- Fix "Import cancelled" message appearing after successful import
## v2.31.1 - 138 (2024-08-24)
- Fix back gesture on main screen dismissing keyboard and search on Android 13+
## v2.31.0 - 137 (2024-07-26)
- Allow long store names in preview to split over multiple lines
- Option to use front of back image in thumbnail menu
- Minor import/export fixes
- Minor UI fixes
## v2.30.0 - 136 (2024-06-18)
- Support for creating a card when sharing plain text
- Display image type instead of barcode below images
- Fix possible crash when trying to import a backup from the Nextcloud app
- Improved support for devices without camera
## v2.29.1 - 135 (2024-05-19)
- Various fixes and improvements to balance handling
## v2.29.0 - 134 (2024-04-19)
- Support for scanning PDF files for barcodes
- Support for image files with multiple barcodes
- Minor UI fixes
## v2.28.0 - 133 (2024-03-08)
- Target Android 14
- Open card icon in gallery on touch
- Improve design of Photos tab in edit view
- Update spending screen to also support receiving
## v2.27.0 - 132 (2024-01-30)
- Refine "Add card" workflow
- Validation flow improvements
- Fix edge case causing invalid UI state when toggling showing archive
- Use theme or card colour for navigation bar (Android 8.1+)
- Updated validity and expiry date selector
- Add option to always rotate (ignoring system settings)
## v2.26.0 - 131 (2023-09-14)
- Move "Archive mode" into "Display options" (previously "Show details") menu
- Android 13 per-app language support
- Embed privacy policy, changelog and license in the app
## v2.25.3 - 130 (2023-08-25)
- Minor UI fixes
- Fix valid from and expiry dates being reset when rotating the card editing screen
- Fix crash when rotating screen while the color picker is shown
- Stocard import fixes
## v2.25.2 - 129 (2023-07-27)
- Improved Catima importer (fixes cards missing when importing)
- Fix crash when rotating screen while setting valid from/expiry date
- Minor UI tweaks
## v2.25.1 - 128 (2023-07-17)
- Fix rare crash
## v2.25.0 - 127 (2023-07-09)
- Barcode rendering improvements
- Basic interoperability with external apps (Android 6.0+)
- Reorganized settings screen
- Fix importing from some browsers that add a trailing / to the share URL
## v2.24.2 - 126 (2023-06-18)
- Various RTL fixes
## v2.24.1 - 125 (2023-06-11)
- Deal more gracefully with missing header colours
## v2.24.0 - 124 (2023-06-10)
- Support selecting exactly which details to view in card overview
## v2.23.3 - 123 (2023-06-03)
- Minor UI improvements
- Fix new design not being usable on devices with square screens
## v2.23.2 - 122 (2023-05-30)
- Long-press card icon in view activity to change it
- Improve button styling in Groups screen
- Fix long barcode values causing barcode to scale down to nothing
## v2.23.1 - 121 (2023-05-27)
- Update used libraries
## v2.23.0 - 120 (2023-05-25)
- Complete redesign of main and loyalty card view screens
- Material You design for the settings screen
- Fix crash when using "Take a photo" with disabled camera app
## v2.22.1 - 119 (2023-04-14)
- Use Material You colours on more devices (Google library update)
## v2.22.0 - 118 (2023-03-18)
## Unreleased - 118
- Support setting start of card validity
- Fix Stocard import (Stocard's export format changed)
## v2.21.2 - 117 (2023-01-27)
## v2.21.2 - 117
- Remove unnecessary permissions
- Target Android 13
## v2.21.1 - 116 (2022-12-06)
## v2.21.1 - 116
- Fix quick spend dialog not allowing , separator
- Support loading image from file manager
## v2.21.0 - 115 (2022-11-06)
## v2.21.0 - 115
- Open image in gallery on long-press
- Apply Material style to dialogs
- Support creating card by sharing an image to Catima
- Add quick spend button to card screen
## v2.20.0 - 114 (2022-09-21)
## v2.20.0 - 114
- Add Monochrome icon for Android 13
- Improve first launch screen
- Fidme import fixes
## v2.19.0 - 113 (2022-08-14)
## v2.19.0 - 113
- Add previous and next buttons to the loyalty card view
- Fix foreground colour on edit button
- Replace floppy disk save icon with checkmark
## v2.18.2 - 112 (2022-07-29)
## v2.18.2 - 112
- Make the possibility to set a custom header more visible
## v2.18.1 - 111 (2022-07-24)
## v2.18.1 - 111
- Arabic language support
- Display archived card count in group overview
@@ -190,11 +47,11 @@
- Fix crash when leaving cardview in RTL layouts for cards with expiry or balance
- Fix back arrow in card view pointing the wrong way in RTL layouts
## v2.17.1 - 109 (2022-06-28)
## v2.17.1 - 109
- Fix incorrect text colour on "No barcode" button
## v2.17.0 - 108 (2022-06-24)
## v2.17.0 - 108
- Add card duplication feature
- Don't allow choosing expiry before 1970 (they never worked anyway)

View File

@@ -1,15 +1,13 @@
# How to Submit Patches to the Catima Project
How to Submit Patches to the Catima Project
===============================================================================
https://github.com/TheLastProject/Catima
This document is intended to act as a guide to help you contribute to the
Catima project. It is not perfect, and there will always be exceptions
Catima project. It is not perfect, and there will always be exceptions
to the rules described here, but by following the instructions below you
should have a much easier time getting your work merged with the upstream
project.
When contributing, you certify that you agree to and have the rights to submit
your contribution under the project's license and understand that git will
store your name and email address in project history indefinitely.
## Translation Changes
Translation changes are managed through [Weblate](https://hosted.weblate.org/projects/catima/).
@@ -59,6 +57,44 @@ if you can describe/include a reproducer for the problem in the description as
well as instructions on how to test for the bug and verify that it has been
fixed.
### Sign Your Work
The sign-off is a simple line at the end of the patch description, which
certifies that you wrote it or otherwise have the right to pass it on as an
open-source patch. The "Developer's Certificate of Origin" pledge is taken
from the Linux Kernel and the rules are pretty simple:
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
... then you just add a line to the bottom of your patch description, with
your real name, saying:
Signed-off-by: Random J Developer <random@developer.example.org>
### Submit Patch(es) for Review
Finally, you will need to submit your patches so that they can be reviewed

View File

@@ -1,45 +1,43 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.7)
base64
nkf
CFPropertyList (3.0.6)
rexml
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
artifactory (3.0.17)
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.1020.0)
aws-sdk-core (3.214.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
aws-eventstream (1.2.0)
aws-partitions (1.701.0)
aws-sdk-core (3.170.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.96.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.176.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-kms (1.62.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.119.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.10.1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.2)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
declarative (0.0.20)
digest-crc (0.6.5)
digest-crc (0.6.4)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.112.0)
faraday (1.10.4)
excon (0.98.0)
faraday (1.10.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@@ -60,22 +58,22 @@ GEM
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.2)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.1)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.3.1)
fastlane (2.226.0)
fastimage (2.2.6)
fastlane (2.211.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored (~> 1.2)
colored
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
@@ -84,38 +82,33 @@ GEM
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
multipart-post (~> 2.0.0)
naturally (~> 2.2)
optparse (>= 0.1.1, < 1.0.0)
optparse (~> 0.1.1)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.5)
security (= 0.1.3)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3)
terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.4.0)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.3)
google-apis-androidpublisher_v3 (0.32.0)
google-apis-core (>= 0.9.1, < 2.a)
google-apis-core (0.10.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
@@ -123,63 +116,64 @@ GEM
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.7.1)
google-cloud-env (>= 1.0, < 3.a)
webrick
google-apis-iamcredentials_v1 (0.16.0)
google-apis-core (>= 0.9.1, < 2.a)
google-apis-playcustomapp_v1 (0.12.0)
google-apis-core (>= 0.9.1, < 2.a)
google-apis-storage_v1 (0.19.0)
google-apis-core (>= 0.9.0, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.4.0)
google-cloud-storage (1.47.0)
google-cloud-errors (1.3.0)
google-cloud-storage (1.44.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.31.0)
google-apis-storage_v1 (~> 0.19.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.8.1)
googleauth (1.3.0)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.8)
http-cookie (1.0.5)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.2)
json (2.9.0)
jwt (2.9.3)
base64
mini_magick (4.13.2)
mini_mime (1.1.5)
json (2.6.3)
jwt (2.6.0)
memoist (0.16.2)
mini_magick (4.12.0)
mini_mime (1.1.2)
multi_json (1.15.0)
multipart-post (2.4.1)
nanaimo (0.4.0)
multipart-post (2.0.0)
nanaimo (0.3.0)
naturally (2.2.1)
nkf (0.2.0)
optparse (0.6.0)
optparse (0.1.1)
os (1.1.4)
plist (3.7.1)
public_suffix (6.0.1)
rake (13.2.1)
plist (3.6.0)
public_suffix (5.0.1)
rake (13.0.6)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.3.9)
rouge (3.28.0)
rexml (3.2.5)
rouge (2.0.7)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.5)
signet (0.19.0)
security (0.1.3)
signet (0.17.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
@@ -187,27 +181,30 @@ GEM
simctl (1.6.10)
CFPropertyList
naturally
sysrandom (1.0.5)
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.2)
tty-screen (0.8.1)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unicode-display_width (2.6.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (1.8.0)
webrick (1.8.1)
word_wrap (1.0.0)
xcodeproj (1.27.0)
xcodeproj (1.22.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)
xcpretty (0.4.0)
rouge (~> 3.28.0)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
@@ -218,4 +215,4 @@ DEPENDENCIES
fastlane
BUNDLED WITH
2.5.22
2.3.26

View File

@@ -1,18 +0,0 @@
**Last updated**
August 30 2023
# Privacy Policy
Catima does not collect or transmit any personal information.
To ensure correct app functionality, we require access to the following:
- Camera: We need access to your camera to be able to scan barcodes. The app can still be used when camera access is denied, but you will have to manually type the barcode information.
- Storage (Android 5 and 6 only): We need access to your device storage to create or import backups. The app can still be used when storage access is denied, but you will not be able to create or import backups.
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.
# 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.
# Contact us
If you have any questions regarding privacy while using the Application, or have questions about our practices, please contact us via email at catima.g9ex3@hackerchick.me.

View File

@@ -1,13 +0,0 @@
# Security Policy
Catima is designed to use as little permissions as possible to limit both the attack surface as well as the damage that can be done when abusing a security flaw.
## Supported Versions
Only the most recent stable release is supported.
## Reporting a Vulnerability
Security vulnerabilities can be reported through [GitHub Security Advisories](https://github.com/CatimaLoyalty/Android/security/advisories) or [the contact info written on my personal website](https://sylviavanos.nl/#contact). Currently, Matrix is the only end-to-end encrypted option.
Please note that only security vulnerabilities in Catima should be reported as stated above. For other issues, including antivirus false positives and malicious applications trying to trick people into granting them Catima's "Read Cards" permission, please use [regular issues](https://github.com/CatimaLoyalty/Android/issues).

129
app/build.gradle Normal file
View File

@@ -0,0 +1,129 @@
import com.github.spotbugs.snom.SpotBugsTask
plugins {
id 'com.android.application'
id 'com.github.spotbugs'
}
spotbugs {
ignoreFailures = false
effort = 'max'
excludeFilter = file("./config/spotbugs/exclude.xml")
reportsDir = file("$buildDir/reports/spotbugs/")
}
android {
compileSdk 33
defaultConfig {
applicationId "me.hackerchick.catima"
minSdk 21
targetSdk 33
versionCode 117
versionName "2.21.2"
vectorDrawables.useSupportLibrary true
multiDexEnabled true
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
resValue "string", "app_name", "Catima"
}
debug {
applicationIdSuffix ".debug"
resValue "string", "app_name", "Catima Debug"
}
}
buildFeatures {
viewBinding true
}
bundle {
language {
enableSplit = false
}
}
compileOptions {
encoding "UTF-8"
// Flag to enable support for the new language APIs
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
lintOptions {
lintConfig file("lint.xml")
}
sourceSets {
test {
resources.srcDirs += ['src/test/res']
}
}
// Starting with Android Studio 3 Robolectric is unable to find resources.
// The following allows it to find the resources.
testOptions {
unitTests {
all {
testLogging {
events 'started', 'passed', 'skipped', 'failed'
}
}
includeAndroidResources true
}
}
}
dependencies {
// AndroidX
implementation 'androidx.appcompat:appcompat:1.6.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.exifinterface:exifinterface:1.3.5'
implementation 'androidx.palette:palette:1.0.0'
implementation 'androidx.preference:preference:1.2.0'
implementation 'com.google.android.material:material:1.6.1'
implementation 'com.github.yalantis:ucrop:2.2.8'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6'
// Splash Screen
implementation 'androidx.core:core-splashscreen:1.0.0'
// Third-party
implementation 'com.journeyapps:zxing-android-embedded:4.3.0@aar'
implementation 'com.google.zxing:core:3.5.1'
implementation 'org.apache.commons:commons-csv:1.9.0'
implementation 'com.jaredrummler:colorpicker:1.1.0'
implementation 'com.github.invissvenska:NumberPickerPreference:1.0.4'
implementation 'net.lingala.zip4j:zip4j:2.11.5'
// SpotBugs
implementation 'io.wcm.tooling.spotbugs:io.wcm.tooling.spotbugs.annotations:1.0.0'
// Testing
testImplementation 'androidx.test:core:1.5.0'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.9.2'
}
tasks.withType(SpotBugsTask) {
description 'Run spotbugs'
group 'verification'
//classes = fileTree('build/intermediates/javac/debug/compileDebugJavaWithJavac/classes')
//source = fileTree('src/main/java')
//classpath = files()
reports {
xml.enabled = false
html.enabled = true
}
}

View File

@@ -1,176 +0,0 @@
import com.android.build.gradle.internal.tasks.factory.dependsOn
import com.github.spotbugs.snom.SpotBugsTask
plugins {
id("com.android.application")
id("com.github.spotbugs")
id("org.jetbrains.kotlin.android")
}
spotbugs {
ignoreFailures.set(false)
setEffort("max")
excludeFilter.set(file("./config/spotbugs/exclude.xml"))
reportsDir.set(layout.buildDirectory.file("reports/spotbugs/").get().asFile)
}
android {
namespace = "protect.card_locker"
compileSdk = 34
defaultConfig {
applicationId = "me.hackerchick.catima"
minSdk = 21
targetSdk = 34
versionCode = 144
versionName = "2.34.2"
vectorDrawables.useSupportLibrary = true
multiDexEnabled = true
resourceConfigurations += listOf("ar", "bg", "bn", "bn-rIN", "bs", "cs", "da", "de", "el-rGR", "en", "eo", "es", "es-rAR", "et", "fi", "fr", "gl", "he-rIL", "hi", "hr", "hu", "in-rID", "is", "it", "ja", "ko", "lt", "lv", "nb-rNO", "nl", "oc", "pl", "pt-rBR", "pt-rPT", "ro-rRO", "ru", "sk", "sl", "sr", "sv", "ta", "tr", "uk", "vi", "zh-rCN", "zh-rTW")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("boolean", "showDonate", "true")
buildConfigField("boolean", "showRateOnGooglePlay", "false")
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android.txt"),
"proguard-rules.pro"
)
}
debug {
applicationIdSuffix = ".debug"
}
}
buildFeatures {
buildConfig = true
viewBinding = true
}
flavorDimensions.add("type")
productFlavors {
create("foss") {
dimension = "type"
isDefault = true
}
create("gplay") {
dimension = "type"
// Google doesn't allow donation links
buildConfigField("boolean", "showDonate", "false")
buildConfigField("boolean", "showRateOnGooglePlay", "true")
}
}
bundle {
language {
enableSplit = false
}
}
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")
}
}
// Starting with Android Studio 3 Robolectric is unable to find resources.
// The following allows it to find the resources.
testOptions.unitTests.isIncludeAndroidResources = true
tasks.withType<Test>().configureEach {
testLogging {
events("started", "passed", "skipped", "failed")
}
}
lint {
lintConfig = file("lint.xml")
}
kotlinOptions {
jvmTarget = "17"
}
}
dependencies {
// AndroidX
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.constraintlayout:constraintlayout:2.2.0")
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("androidx.exifinterface:exifinterface:1.3.7")
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.4")
// Third-party
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")
// SpotBugs
implementation("io.wcm.tooling.spotbugs:io.wcm.tooling.spotbugs.annotations:1.0.0")
// Testing
val androidXTestVersion = "1.6.1"
val junitVersion = "4.13.2"
testImplementation("androidx.test:core:$androidXTestVersion")
testImplementation("junit:junit:$junitVersion")
testImplementation("org.robolectric:robolectric:4.14.1")
androidTestImplementation("androidx.test:core:$androidXTestVersion")
androidTestImplementation("junit:junit:$junitVersion")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test:runner:$androidXTestVersion")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
}
tasks.withType<SpotBugsTask>().configureEach {
description = "Run spotbugs"
group = "verification"
//classes = fileTree("build/intermediates/javac/debug/compileDebugJavaWithJavac/classes")
//source = fileTree("src/main/java")
//classpath = files()
reports.maybeCreate("xml").required.set(false)
reports.maybeCreate("html").required.set(true)
}
tasks.register("copyRawResFiles", Copy::class) {
from(
layout.projectDirectory.file("../CHANGELOG.md"),
layout.projectDirectory.file("../PRIVACY.md")
)
into(layout.projectDirectory.dir("src/main/res/raw"))
rename { it.lowercase() }
}.also {
tasks.preBuild.dependsOn(it)
tasks.getByName<Delete>("clean") {
val filesNamesToDelete = listOf("CHANGELOG", "PRIVACY")
filesNamesToDelete.forEach { fileName ->
delete(layout.projectDirectory.file("src/main/res/raw/${fileName.lowercase()}.md"))
}
}
}

View File

@@ -2,7 +2,7 @@
# By default, the flags in this file are appended to flags specified
# in /Users/brarcher/Library/Android/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.kts.
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

View File

@@ -1,67 +0,0 @@
package protect.card_locker;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withChild;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import androidx.appcompat.widget.Toolbar;
import androidx.test.core.app.ActivityScenario;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.uiautomator.UiDevice;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class MainActivitySearchViewTest {
@Test
public void whenSearchViewIsExpandedAndBackIsPressedThenMenuItemShouldNotBeCollapsed() {
String query = "random arbitrary text";
try (ActivityScenario<MainActivity> mainActivityScenario = ActivityScenario.launch(MainActivity.class)) {
mainActivityScenario.onActivity(this::makeSearchMenuItemVisible);
onView(withId(R.id.action_search)).perform(click());
onView(withId(androidx.appcompat.R.id.search_src_text)).perform(typeText(query));
pressBack();
onView(withId(androidx.appcompat.R.id.search_src_text)).check(matches(withText(query)));
mainActivityScenario.onActivity(activity -> assertEquals(query, activity.mFilter));
}
}
@Test
public void whenSearchViewIsExpandedThenItShouldOnlyBeCollapsedWhenBackIsPressedTwice() {
try (ActivityScenario<MainActivity> mainActivityScenario = ActivityScenario.launch(MainActivity.class)) {
mainActivityScenario.onActivity(this::makeSearchMenuItemVisible);
onView(withId(R.id.action_search)).perform(click());
pressBack();
onView(withId(androidx.appcompat.R.id.search_src_text)).check(matches(isDisplayed()));
pressBack();
onView(withId(android.R.id.content)).check(matches(is(not(withChild(withId(androidx.appcompat.R.id.search_src_text))))));
}
}
private void makeSearchMenuItemVisible(MainActivity activity) {
Toolbar toolbar = activity.findViewById(R.id.toolbar);
toolbar.getMenu().findItem(R.id.action_search).setVisible(true);
}
private void pressBack() {
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).pressBack();
}
}

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">تصحيح Catima</string>
</resources>

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima Debug</string>
</resources>

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima Debug</string>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Αποσφαλμάτωση Catima</string>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima Debug</string>
</resources>

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Depuración de Catima</string>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima Debug</string>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima-vianmääritys</string>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Débogage de Catima</string>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Depuración de Catima</string>
</resources>

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">कैटिमा डीबग</string>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima Debug</string>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima Debug</string>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima Debug</string>
</resources>

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima Debug</string>
</resources>

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">ಕ್ಯಾಟಿಮಾ ಡೀಬಗ್</string>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima 디버그</string>
</resources>

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima atkļūdošana</string>
</resources>

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima-avlusing</string>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima-foutopsporing</string>
</resources>

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima Debug</string>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Depuração do Catima</string>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Depuração Catima</string>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Depanare Catima</string>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Отладка Catima</string>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima Debug</string>
</resources>

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima Debug</string>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima Hata Ayaklama</string>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima Debug</string>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Gỡ lỗi Catima</string>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima 调试</string>
</resources>

View File

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

View File

@@ -1,4 +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 Debug</string>
</resources>

View File

@@ -1,13 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<permission
android:description="@string/permissionReadCardsDescription"
android:icon="@drawable/ic_launcher_foreground"
android:label="@string/permissionReadCardsLabel"
android:name="${applicationId}.READ_CARDS"
android:protectionLevel="dangerous" />
xmlns:tools="http://schemas.android.com/tools"
package="protect.card_locker">
<uses-sdk tools:overrideLibrary="com.google.zxing.client.android" />
@@ -16,7 +10,7 @@
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
android:required="true" />
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false" />
@@ -24,40 +18,25 @@
<application
android:name=".LoyaltyCardLockerApplication"
android:allowBackup="true"
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:localeConfig="@xml/locales_config">
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.App.Starting">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="content"/>
<data android:host="*"/>
<data android:mimeType="image/*" />
<data android:mimeType="application/pdf" />
<data android:mimeType="application/vnd.apple.pkpass" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="application/pdf" />
<data android:mimeType="application/vnd.apple.pkpass" />
</intent-filter>
</activity>
<activity
@@ -125,12 +104,10 @@
android:name=".preferences.SettingsActivity"
android:label="@string/settings"
android:theme="@style/AppTheme.NoActionBar" />
<!-- 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 -->
@@ -179,12 +156,6 @@
android:name=".UCropWrapper"
android:theme="@style/AppTheme.NoActionBar" />
<provider
android:name=".contentprovider.CardsContentProvider"
android:authorities="${applicationId}.contentprovider.cards"
android:exported="true"
android:readPermission="${applicationId}.READ_CARDS"/>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}"
@@ -195,11 +166,10 @@
android:resource="@xml/file_provider_paths" />
</provider>
<service android:name=".CardsOnPowerScreenService" android:label="@string/app_name"
android:permission="android.permission.BIND_CONTROLS" android:exported="true"
tools:targetApi="r">
android:permission="android.permission.BIND_CONTROLS" android:exported="true">
<intent-filter>
<action android:name="android.service.controls.ControlsProviderService" />
</intent-filter>
</service>
</application>
</manifest>
</manifest>

View File

@@ -1,17 +1,13 @@
package protect.card_locker;
import android.os.Bundle;
import android.text.Spanned;
import android.view.MenuItem;
import android.view.View;
import android.widget.ScrollView;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import protect.card_locker.databinding.AboutActivityBinding;
public class AboutActivity extends CatimaAppCompatActivity {
@@ -32,7 +28,7 @@ public class AboutActivity extends CatimaAppCompatActivity {
enableToolbarBackButton();
TextView copyright = binding.creditsSub;
copyright.setText(content.getCopyrightShort());
copyright.setText(content.getCopyright());
TextView versionHistory = binding.versionHistorySub;
versionHistory.setText(content.getVersionHistory());
@@ -43,12 +39,6 @@ public class AboutActivity extends CatimaAppCompatActivity {
binding.privacy.setTag("https://catima.app/privacy-policy/");
binding.reportError.setTag("https://github.com/CatimaLoyalty/Android/issues");
binding.rate.setTag("https://play.google.com/store/apps/details?id=me.hackerchick.catima");
binding.donate.setTag("https://catima.app/donate");
// Hide Google Play rate button if not on Google Play
binding.rate.setVisibility(BuildConfig.showRateOnGooglePlay ? View.VISIBLE : View.GONE);
// Hide donate button on Google Play (Google Play doesn't allow donation links)
binding.donate.setVisibility(BuildConfig.showDonate ? View.VISIBLE : View.GONE);
bindClickListeners();
}
@@ -71,14 +61,19 @@ public class AboutActivity extends CatimaAppCompatActivity {
}
private void bindClickListeners() {
binding.versionHistory.setOnClickListener(this::showHistory);
binding.translate.setOnClickListener(this::openExternalBrowser);
binding.license.setOnClickListener(this::showLicense);
binding.repo.setOnClickListener(this::openExternalBrowser);
binding.privacy.setOnClickListener(this::showPrivacy);
binding.reportError.setOnClickListener(this::openExternalBrowser);
binding.rate.setOnClickListener(this::openExternalBrowser);
binding.donate.setOnClickListener(this::openExternalBrowser);
View.OnClickListener openExternalBrowser = view -> {
Object tag = view.getTag();
if (tag instanceof String && ((String) tag).startsWith("https://")) {
(new OpenWebLinkHandler()).openBrowser(this, (String) tag);
}
};
binding.versionHistory.setOnClickListener(openExternalBrowser);
binding.translate.setOnClickListener(openExternalBrowser);
binding.license.setOnClickListener(openExternalBrowser);
binding.repo.setOnClickListener(openExternalBrowser);
binding.privacy.setOnClickListener(openExternalBrowser);
binding.reportError.setOnClickListener(openExternalBrowser);
binding.rate.setOnClickListener(openExternalBrowser);
binding.credits.setOnClickListener(view -> showCredits());
}
@@ -91,56 +86,14 @@ public class AboutActivity extends CatimaAppCompatActivity {
binding.privacy.setOnClickListener(null);
binding.reportError.setOnClickListener(null);
binding.rate.setOnClickListener(null);
binding.donate.setOnClickListener(null);
binding.credits.setOnClickListener(null);
}
private void showCredits() {
showHTML(R.string.credits, content.getContributorInfo(), null);
}
private void showHistory(View view) {
showHTML(R.string.version_history, content.getHistoryInfo(), view);
}
private void showLicense(View view) {
showHTML(R.string.license, content.getLicenseInfo(), view);
}
private void showPrivacy(View view) {
showHTML(R.string.privacy_policy, content.getPrivacyInfo(), view);
}
private void showHTML(@StringRes int title, final Spanned text, @Nullable View view) {
int dialogContentPadding = getResources().getDimensionPixelSize(R.dimen.alert_dialog_content_padding);
TextView textView = new TextView(this);
textView.setText(text);
Utils.makeTextViewLinksClickable(textView, text);
ScrollView scrollView = new ScrollView(this);
scrollView.addView(textView);
scrollView.setPadding(dialogContentPadding, dialogContentPadding / 2, dialogContentPadding, 0);
// Create dialog
MaterialAlertDialogBuilder materialAlertDialogBuilder = new MaterialAlertDialogBuilder(this);
materialAlertDialogBuilder
.setTitle(title)
.setView(scrollView)
.setPositiveButton(R.string.ok, null);
// Add View online button if an URL is linked to this view
if (view != null && view.getTag() != null) {
materialAlertDialogBuilder.setNeutralButton(R.string.view_online, (dialog, which) -> openExternalBrowser(view));
}
// Show dialog
materialAlertDialogBuilder.show();
}
private void openExternalBrowser(View view) {
Object tag = view.getTag();
if (tag instanceof String && ((String) tag).startsWith("https://")) {
(new OpenWebLinkHandler()).openBrowser(this, (String) tag);
}
new MaterialAlertDialogBuilder(this)
.setTitle(R.string.credits)
.setMessage(content.getContributorInfo())
.setPositiveButton(R.string.ok, null)
.show();
}
}

View File

@@ -3,7 +3,6 @@ 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;
@@ -51,10 +50,6 @@ public class AboutContent {
return String.format(context.getString(R.string.app_copyright_fmt), getCurrentYear());
}
public String getCopyrightShort() {
return context.getString(R.string.app_copyright_short);
}
public String getContributors() {
String contributors;
try {
@@ -65,38 +60,6 @@ public class AboutContent {
return contributors.replace("\n", "<br />");
}
public String getHistory() {
String versionHistory;
try {
versionHistory = Utils.readTextFile(context, R.raw.changelog)
.replace("# Changelog\n\n", "");
} catch (IOException ignored) {
return "";
}
return Utils.linkify(Utils.basicMDToHTML(versionHistory))
.replace("\n", "<br />");
}
public String getLicense() {
try {
return Utils.readTextFile(context, R.raw.license);
} catch (IOException ignored) {
return "";
}
}
public String getPrivacy() {
String privacyPolicy;
try {
privacyPolicy = Utils.readTextFile(context, R.raw.privacy)
.replace("# Privacy Policy\n", "");
} catch (IOException ignored) {
return "";
}
return Utils.linkify(Utils.basicMDToHTML(privacyPolicy))
.replace("\n", "<br />");
}
public String getThirdPartyLibraries() {
final List<ThirdPartyInfo> usedLibraries = new ArrayList<>();
usedLibraries.add(new ThirdPartyInfo("Color Picker", "https://github.com/jaredrummler/ColorPicker", "Apache 2.0"));
@@ -129,31 +92,17 @@ public class AboutContent {
return result.toString();
}
public Spanned getContributorInfo() {
public String getContributorInfo() {
StringBuilder contributorInfo = new StringBuilder();
contributorInfo.append(getCopyright());
contributorInfo.append("<br/><br/>");
contributorInfo.append(HtmlCompat.fromHtml(String.format(context.getString(R.string.app_contributors), getContributors()), HtmlCompat.FROM_HTML_MODE_COMPACT));
contributorInfo.append("\n\n");
contributorInfo.append(context.getString(R.string.app_copyright_old));
contributorInfo.append("<br/><br/>");
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), getThirdPartyLibraries()));
contributorInfo.append("<br/><br/>");
contributorInfo.append(String.format(context.getString(R.string.app_resources), getUsedThirdPartyAssets()));
contributorInfo.append("\n\n");
contributorInfo.append(HtmlCompat.fromHtml(String.format(context.getString(R.string.app_libraries), getThirdPartyLibraries()), HtmlCompat.FROM_HTML_MODE_COMPACT));
contributorInfo.append("\n\n");
contributorInfo.append(HtmlCompat.fromHtml(String.format(context.getString(R.string.app_resources), getUsedThirdPartyAssets()), HtmlCompat.FROM_HTML_MODE_COMPACT));
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);
return contributorInfo.toString();
}
public String getVersionHistory() {

View File

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

View File

@@ -41,15 +41,13 @@ public class BarcodeImageWriterTask implements CompatCallable<Bitmap> {
private final CatimaBarcode format;
private final int imageHeight;
private final int imageWidth;
private final int imagePadding;
private final boolean widthPadding;
private final boolean showFallback;
private final BarcodeImageWriterResultCallback callback;
private final Runnable callback;
BarcodeImageWriterTask(
Context context, ImageView imageView, String cardIdString,
CatimaBarcode barcodeFormat, TextView textView,
boolean showFallback, BarcodeImageWriterResultCallback callback, boolean roundCornerPadding
boolean showFallback, Runnable callback, boolean roundCornerPadding
) {
mContext = context;
@@ -63,39 +61,32 @@ public class BarcodeImageWriterTask implements CompatCallable<Bitmap> {
cardId = cardIdString;
format = barcodeFormat;
int imageViewHeight = imageView.getHeight();
int imageViewWidth = imageView.getWidth();
int padding = 0;
// Some barcodes already have internal whitespace and shouldn't get extra padding
// TODO: Get rid of this hack by somehow detecting this extra whitespace
if (roundCornerPadding && !barcodeFormat.hasInternalPadding()) {
imagePadding = Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, context.getResources().getDisplayMetrics()));
} else {
imagePadding = 0;
}
if (format.isSquare() && imageViewWidth > imageViewHeight) {
imageViewWidth -= imagePadding;
widthPadding = true;
} else {
imageViewHeight -= imagePadding;
widthPadding = false;
padding = Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, context.getResources().getDisplayMetrics()));
}
final int MAX_WIDTH = getMaxWidth(format);
if (format.isSquare()) {
imageHeight = imageWidth = Math.min(imageViewHeight, Math.min(MAX_WIDTH, imageViewWidth));
} else if (imageView.getWidth() < MAX_WIDTH) {
imageHeight = imageViewHeight;
imageWidth = imageViewWidth;
int tempImageHeight;
int tempImageWidth;
if (imageView.getWidth() < MAX_WIDTH) {
tempImageHeight = imageView.getHeight();
tempImageWidth = imageView.getWidth();
} else {
// Scale down the image to reduce the memory needed to produce it
imageWidth = MAX_WIDTH;
double ratio = (double) MAX_WIDTH / (double) imageViewWidth;
imageHeight = (int) (imageViewHeight * ratio);
tempImageWidth = MAX_WIDTH;
double ratio = (double) MAX_WIDTH / (double) imageView.getWidth();
tempImageHeight = (int) (imageView.getHeight() * ratio);
}
// Ensure space for padding if wanted
imageWidth = tempImageWidth;
imageHeight = tempImageHeight - padding;
this.showFallback = showFallback;
}
@@ -103,15 +94,12 @@ public class BarcodeImageWriterTask implements CompatCallable<Bitmap> {
switch (format.format()) {
// 2D barcodes
case AZTEC:
case DATA_MATRIX:
case MAXICODE:
case PDF_417:
case QR_CODE:
return MAX_WIDTH_2D;
// 2D but rectangular versions get blurry otherwise
case DATA_MATRIX:
return MAX_WIDTH_1D;
// 1D barcodes:
case CODABAR:
case CODE_39:
@@ -273,11 +261,6 @@ public class BarcodeImageWriterTask implements CompatCallable<Bitmap> {
if (result != null) {
Log.i(TAG, "Displaying barcode");
if (widthPadding) {
imageView.setPadding(imagePadding / 2, 0, imagePadding / 2, 0);
} else {
imageView.setPadding(0, imagePadding / 2, 0, imagePadding / 2);
}
imageView.setVisibility(View.VISIBLE);
if (isSuccesful) {
@@ -299,7 +282,7 @@ public class BarcodeImageWriterTask implements CompatCallable<Bitmap> {
}
if (callback != null) {
callback.onBarcodeImageWriterResult(isSuccesful);
callback.run();
}
}

View File

@@ -12,12 +12,13 @@ import android.widget.EditText;
import android.widget.ListView;
import android.widget.Toast;
import androidx.appcompat.widget.Toolbar;
import com.google.zxing.BarcodeFormat;
import java.util.ArrayList;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.Toolbar;
import protect.card_locker.databinding.BarcodeSelectorActivityBinding;
/**
@@ -65,13 +66,17 @@ public class BarcodeSelectorActivity extends CatimaAppCompatActivity implements
runOnUiThread(() -> {
generateBarcodes(s.toString());
View noBarcodeButtonView = binding.noBarcode;
setButtonListener(noBarcodeButtonView, s.toString());
noBarcodeButtonView.setEnabled(s.length() > 0);
});
}, INPUT_DELAY);
}
});
final Bundle b = getIntent().getExtras();
final String initialCardId = b != null ? b.getString(LoyaltyCard.BUNDLE_LOYALTY_CARD_CARD_ID) : null;
final String initialCardId = b != null ? b.getString("initialCardId") : null;
if (initialCardId != null) {
cardId.setText(initialCardId);
@@ -90,6 +95,17 @@ public class BarcodeSelectorActivity extends CatimaAppCompatActivity implements
mAdapter.setBarcodes(barcodes);
}
private void setButtonListener(final View button, final String cardId) {
button.setOnClickListener(view -> {
Log.d(TAG, "Selected no barcode");
Intent result = new Intent();
result.putExtra(BARCODE_FORMAT, "");
result.putExtra(BARCODE_CONTENTS, cardId);
BarcodeSelectorActivity.this.setResult(RESULT_OK, result);
finish();
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {

View File

@@ -0,0 +1,23 @@
package protect.card_locker;
public class BarcodeValues {
private final String mFormat;
private final String mContent;
public BarcodeValues(String format, String content) {
mFormat = format;
mContent = content;
}
public String format() {
return mFormat;
}
public String content() {
return mContent;
}
public boolean isEmpty() {
return mFormat == null && mContent == null;
}
}

View File

@@ -12,15 +12,15 @@ import androidx.appcompat.widget.Toolbar;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import protect.card_locker.databinding.CardShortcutConfigureActivityBinding;
import protect.card_locker.preferences.Settings;
import protect.card_locker.databinding.SimpleToolbarListActivityBinding;
/**
* The configuration screen for creating a shortcut.
*/
public class CardShortcutConfigure extends CatimaAppCompatActivity implements LoyaltyCardCursorAdapter.CardAdapterListener {
private CardShortcutConfigureActivityBinding binding;
private SimpleToolbarListActivityBinding binding;
static final String TAG = "Catima";
private SQLiteDatabase mDatabase;
private LoyaltyCardCursorAdapter mAdapter;
@@ -28,7 +28,7 @@ public class CardShortcutConfigure extends CatimaAppCompatActivity implements Lo
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
binding = CardShortcutConfigureActivityBinding.inflate(getLayoutInflater());
binding = SimpleToolbarListActivityBinding.inflate(getLayoutInflater());
mDatabase = new DBHelper(this).getReadableDatabase();
// Set the result to CANCELED. This will cause nothing to happen if the
@@ -47,26 +47,27 @@ public class CardShortcutConfigure extends CatimaAppCompatActivity implements Lo
finish();
}
Cursor cardCursor = DBHelper.getLoyaltyCardCursor(mDatabase, DBHelper.LoyaltyCardArchiveFilter.All);
mAdapter = new LoyaltyCardCursorAdapter(this, cardCursor, this, null);
binding.list.setAdapter(mAdapter);
}
@Override
protected void onResume() {
super.onResume();
var layoutManager = (GridLayoutManager) binding.list.getLayoutManager();
if (layoutManager != null) {
var settings = new Settings(this);
layoutManager.setSpanCount(settings.getPreferredColumnCount());
// If all cards are archived, bail
if (DBHelper.getArchivedCardsCount(mDatabase) == cardCount) {
Toast.makeText(this, R.string.noUnarchivedCardsMessage, Toast.LENGTH_LONG).show();
finish();
}
final RecyclerView cardList = binding.list;
GridLayoutManager layoutManager = (GridLayoutManager) cardList.getLayoutManager();
if (layoutManager != null) {
layoutManager.setSpanCount(getResources().getInteger(R.integer.main_view_card_columns));
}
Cursor cardCursor = DBHelper.getLoyaltyCardCursor(mDatabase, DBHelper.LoyaltyCardArchiveFilter.Unarchived);
mAdapter = new LoyaltyCardCursorAdapter(this, cardCursor, this);
cardList.setAdapter(mAdapter);
}
private void onClickAction(int position) {
Cursor selected = DBHelper.getLoyaltyCardCursor(mDatabase, DBHelper.LoyaltyCardArchiveFilter.All);
Cursor selected = DBHelper.getLoyaltyCardCursor(mDatabase, DBHelper.LoyaltyCardArchiveFilter.Unarchived);
selected.moveToPosition(position);
LoyaltyCard loyaltyCard = LoyaltyCard.fromCursor(CardShortcutConfigure.this, selected);
LoyaltyCard loyaltyCard = LoyaltyCard.toLoyaltyCard(selected);
Log.d(TAG, "Creating shortcut for card " + loyaltyCard.store + "," + loyaltyCard.id);
@@ -80,7 +81,7 @@ public class CardShortcutConfigure extends CatimaAppCompatActivity implements Lo
@Override
public boolean onCreateOptionsMenu(Menu inputMenu) {
getMenuInflater().inflate(R.menu.card_details_menu, inputMenu);
Utils.updateMenuCardDetailsButtonState(inputMenu.findItem(R.id.action_unfold), mAdapter.showingDetails());
return super.onCreateOptionsMenu(inputMenu);
}
@@ -88,8 +89,8 @@ public class CardShortcutConfigure extends CatimaAppCompatActivity implements Lo
public boolean onOptionsItemSelected(MenuItem inputItem) {
int id = inputItem.getItemId();
if (id == R.id.action_display_options) {
mAdapter.showDisplayOptionsDialog();
if (id == R.id.action_unfold) {
mAdapter.showDetails(!mAdapter.showingDetails());
invalidateOptionsMenu();
return true;

View File

@@ -1,5 +1,6 @@
package protect.card_locker;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
@@ -15,13 +16,13 @@ import android.service.controls.actions.ControlAction;
import android.service.controls.templates.StatelessTemplate;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import java.util.List;
import java.util.concurrent.Flow;
import java.util.function.Consumer;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
@RequiresApi(Build.VERSION_CODES.R)
public class CardsOnPowerScreenService extends ControlsProviderService {
@@ -42,10 +43,10 @@ public class CardsOnPowerScreenService extends ControlsProviderService {
Cursor loyaltyCardCursor = DBHelper.getLoyaltyCardCursor(mDatabase, DBHelper.LoyaltyCardArchiveFilter.Unarchived);
return subscriber -> {
while (loyaltyCardCursor.moveToNext()) {
LoyaltyCard card = LoyaltyCard.fromCursor(this, loyaltyCardCursor);
LoyaltyCard card = LoyaltyCard.toLoyaltyCard(loyaltyCardCursor);
Intent openIntent = new Intent(this, LoyaltyCardViewActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(LoyaltyCardViewActivity.BUNDLE_ID, card.id);
.putExtra("id", card.id);
PendingIntent pendingIntent = PendingIntent.getActivity(getBaseContext(), card.id, openIntent, PendingIntent.FLAG_IMMUTABLE);
subscriber.onNext(
new Control.StatelessBuilder(PREFIX + card.id, pendingIntent)
@@ -68,12 +69,13 @@ public class CardsOnPowerScreenService extends ControlsProviderService {
subscriber.onSubscribe(new NoOpSubscription());
for (String controlId : controlIds) {
Control control;
Integer cardId = this.controlIdToCardId(controlId);
LoyaltyCard card = DBHelper.getLoyaltyCard(this, mDatabase, cardId);
if (card != null) {
try {
Integer cardId = this.controlIdToCardId(controlId);
LoyaltyCard card = DBHelper.getLoyaltyCard(mDatabase, cardId);
Intent openIntent = new Intent(this, LoyaltyCardViewActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(LoyaltyCardViewActivity.BUNDLE_ID, card.id);
.putExtra("id", card.id);
PendingIntent pendingIntent = PendingIntent.getActivity(getBaseContext(), card.id, openIntent, PendingIntent.FLAG_IMMUTABLE);
control = new Control.StatefulBuilder(controlId, pendingIntent)
.setTitle(card.store)
@@ -83,7 +85,7 @@ public class CardsOnPowerScreenService extends ControlsProviderService {
.setControlTemplate(new StatelessTemplate(controlId))
.setCustomIcon(Icon.createWithBitmap(getIcon(this, card)))
.build();
} else {
} catch (NullPointerException ignored) {
Intent mainScreenIntent = new Intent(this, MainActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(getBaseContext(), -1, mainScreenIntent, PendingIntent.FLAG_IMMUTABLE);
@@ -99,7 +101,7 @@ public class CardsOnPowerScreenService extends ControlsProviderService {
}
private Bitmap getIcon(Context context, LoyaltyCard loyaltyCard) {
Bitmap cardIcon = loyaltyCard.getImageThumbnail(context);
Bitmap cardIcon = Utils.retrieveCardImage(context, loyaltyCard.id, ImageLocationType.icon);
if (cardIcon != null) {
return cardIcon;
@@ -129,7 +131,7 @@ public class CardsOnPowerScreenService extends ControlsProviderService {
consumer.accept(ControlAction.RESPONSE_OK);
Intent openIntent = new Intent(this, LoyaltyCardViewActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(LoyaltyCardViewActivity.BUNDLE_ID, controlIdToCardId(controlId));
.putExtra("id", controlIdToCardId(controlId));
startActivity(openIntent);
closePowerScreenOnAndroid11();

View File

@@ -5,7 +5,6 @@ import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -14,8 +13,6 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.core.view.WindowInsetsControllerCompat;
public class CatimaAppCompatActivity extends AppCompatActivity {
protected boolean activityOverridesNavBarColor = false;
@Override
protected void attachBaseContext(Context base) {
// Apply chosen language
@@ -33,31 +30,20 @@ public class CatimaAppCompatActivity extends AppCompatActivity {
super.onPostCreate(savedInstanceState);
// material 3 designer does not consider status bar colors
// XXX changing this in onCreate causes issues with the splash screen activity, so doing this here
Window window = getWindow();
if (window != null) {
boolean darkMode = Utils.isDarkModeEnabled(this);
if (Build.VERSION.SDK_INT >= 23) {
View decorView = window.getDecorView();
WindowInsetsControllerCompat wic = new WindowInsetsControllerCompat(window, decorView);
wic.setAppearanceLightStatusBars(!darkMode);
window.setStatusBarColor(Color.TRANSPARENT);
} else {
// icons are always white back then
window.setStatusBarColor(darkMode ? Color.TRANSPARENT : Color.argb(127, 0, 0, 0));
}
boolean darkMode = Utils.isDarkModeEnabled(this);
if (Build.VERSION.SDK_INT >= 23) {
View decorView = getWindow().getDecorView();
WindowInsetsControllerCompat wic = new WindowInsetsControllerCompat(getWindow(), decorView);
wic.setAppearanceLightStatusBars(!darkMode);
getWindow().setStatusBarColor(Color.TRANSPARENT);
} else {
// icons are always white back then
getWindow().setStatusBarColor(darkMode ? Color.TRANSPARENT : Color.argb(127, 0, 0, 0));
}
// XXX android 9 and below has a nasty rendering bug if the theme was patched earlier
Utils.postPatchColors(this);
}
@Override
protected void onResume() {
super.onResume();
if (!activityOverridesNavBarColor) {
Utils.setNavigationBarColor(this, null, Utils.resolveBackgroundColor(this), !Utils.isDarkModeEnabled(this));
}
}
protected void enableToolbarBackButton() {
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {

View File

@@ -1,7 +1,5 @@
package protect.card_locker;
import androidx.annotation.NonNull;
import com.google.zxing.BarcodeFormat;
import java.util.Arrays;
@@ -47,15 +45,15 @@ public class CatimaBarcode {
mBarcodeFormat = barcodeFormat;
}
public static CatimaBarcode fromBarcode(@NonNull BarcodeFormat barcodeFormat) {
public static CatimaBarcode fromBarcode(BarcodeFormat barcodeFormat) {
return new CatimaBarcode(barcodeFormat);
}
public static CatimaBarcode fromName(@NonNull String name) {
public static CatimaBarcode fromName(String name) {
return new CatimaBarcode(BarcodeFormat.valueOf(name));
}
public static CatimaBarcode fromPrettyName(@NonNull String prettyName) {
public static CatimaBarcode fromPrettyName(String prettyName) {
try {
return new CatimaBarcode(barcodeFormats.get(barcodePrettyNames.indexOf(prettyName)));
} catch (IndexOutOfBoundsException e) {
@@ -69,6 +67,7 @@ public class CatimaBarcode {
public boolean isSquare() {
return mBarcodeFormat == BarcodeFormat.AZTEC
|| mBarcodeFormat == BarcodeFormat.DATA_MATRIX
|| mBarcodeFormat == BarcodeFormat.MAXICODE
|| mBarcodeFormat == BarcodeFormat.QR_CODE;
}

View File

@@ -4,24 +4,22 @@ import android.app.Activity;
import android.content.Context;
import android.widget.Toast;
import androidx.core.util.Consumer;
import com.journeyapps.barcodescanner.CaptureManager;
import com.journeyapps.barcodescanner.DecoratedBarcodeView;
public class CatimaCaptureManager extends CaptureManager {
private final Consumer<String> mErrorCallback;
private final Context mContext;
public CatimaCaptureManager(Activity activity, DecoratedBarcodeView barcodeView, Consumer<String> errorCallback) {
public CatimaCaptureManager(Activity activity, DecoratedBarcodeView barcodeView) {
super(activity, barcodeView);
mErrorCallback = errorCallback;
mContext = activity.getApplicationContext();
}
@Override
protected void displayFrameworkBugMessageAndExit(String message) {
// We don't want to exit, as we also have a enter from card image and add manually button here
// So, instead, we call our error callback
mErrorCallback.accept(message);
// So we show a toast instead
Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
}
}

View File

@@ -16,21 +16,17 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Currency;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
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 = 16;
// NB: changing this value requires a migration
public static final int DEFAULT_ZOOM_LEVEL = 100;
public static final int DATABASE_VERSION = 17;
public static class LoyaltyCardDbGroups {
public static final String TABLE = "groups";
public static final String ID = "_id";
public static final String NAME = "name";
public static final String ORDER = "orderId";
}
@@ -92,7 +88,8 @@ public class DBHelper extends SQLiteOpenHelper {
public void onCreate(SQLiteDatabase db) {
// create table for card groups
db.execSQL("CREATE TABLE " + LoyaltyCardDbGroups.TABLE + "(" +
LoyaltyCardDbGroups.ID + " TEXT primary key not null," +
LoyaltyCardDbGroups.ID + " INTEGER primary key autoincrement," +
LoyaltyCardDbGroups.NAME + " TEXT not null," +
LoyaltyCardDbGroups.ORDER + " INTEGER DEFAULT '0')");
// create table for cards
@@ -111,13 +108,13 @@ public class DBHelper extends SQLiteOpenHelper {
LoyaltyCardDbIds.BARCODE_TYPE + " TEXT," +
LoyaltyCardDbIds.STAR_STATUS + " INTEGER DEFAULT '0'," +
LoyaltyCardDbIds.LAST_USED + " INTEGER DEFAULT '0', " +
LoyaltyCardDbIds.ZOOM_LEVEL + " INTEGER DEFAULT '" + DEFAULT_ZOOM_LEVEL + "', " +
LoyaltyCardDbIds.ZOOM_LEVEL + " INTEGER DEFAULT '100', " +
LoyaltyCardDbIds.ARCHIVE_STATUS + " INTEGER DEFAULT '0' )");
// create associative table for cards in groups
db.execSQL("CREATE TABLE " + LoyaltyCardDbIdsGroups.TABLE + "(" +
LoyaltyCardDbIdsGroups.cardID + " INTEGER," +
LoyaltyCardDbIdsGroups.groupID + " TEXT," +
LoyaltyCardDbIdsGroups.groupID + " INTEGER," +
"primary key (" + LoyaltyCardDbIdsGroups.cardID + "," + LoyaltyCardDbIdsGroups.groupID + "))");
// create FTS search table
@@ -326,21 +323,70 @@ public class DBHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE
+ " ADD COLUMN " + LoyaltyCardDbIds.VALID_FROM + " INTEGER");
}
}
public static Set<String> imageFiles(Context context, final SQLiteDatabase database) {
Set<String> files = new HashSet<>();
Cursor cardCursor = getLoyaltyCardCursor(database);
while (cardCursor.moveToNext()) {
LoyaltyCard card = LoyaltyCard.fromCursor(context, cardCursor);
for (ImageLocationType imageLocationType : ImageLocationType.values()) {
String name = Utils.getCardImageFileName(card.id, imageLocationType);
if (card.getImageForImageLocationType(context, imageLocationType) != null) {
files.add(name);
}
}
if (oldVersion < 17 && newVersion >= 17) {
// SQLite doesn't support modify column
// So we need to create a temp column to change the key of the group table
// https://www.sqlite.org/faq.html#q11
db.beginTransaction();
// Step 1: Migrate LoyaltyCardDbGroups to contain integer ID
db.execSQL("CREATE TEMPORARY TABLE tmp (" +
LoyaltyCardDbGroups.ID + " INTEGER primary key autoincrement," +
LoyaltyCardDbGroups.NAME + " TEXT not null," +
LoyaltyCardDbGroups.ORDER + " INTEGER DEFAULT '0')");
db.execSQL("INSERT INTO tmp (" +
LoyaltyCardDbGroups.NAME + " ," +
LoyaltyCardDbGroups.ORDER + ")" +
" SELECT " +
LoyaltyCardDbGroups.NAME + " ," +
LoyaltyCardDbGroups.ORDER +
" FROM " + LoyaltyCardDbGroups.TABLE);
db.execSQL("DROP TABLE " + LoyaltyCardDbGroups.TABLE);
db.execSQL("CREATE TABLE " + LoyaltyCardDbGroups.TABLE + "(" +
LoyaltyCardDbGroups.ID + " INTEGER primary key autoincrement," +
LoyaltyCardDbGroups.NAME + " TEXT not null," +
LoyaltyCardDbGroups.ORDER + " INTEGER DEFAULT '0')");
db.execSQL("INSERT INTO " + LoyaltyCardDbGroups.TABLE + "(" +
LoyaltyCardDbGroups.ID + " ," +
LoyaltyCardDbGroups.NAME + " ," +
LoyaltyCardDbGroups.ORDER + ")" +
" SELECT " +
LoyaltyCardDbGroups.ID + " ," +
LoyaltyCardDbGroups.NAME + " ," +
LoyaltyCardDbGroups.ORDER +
" FROM tmp");
db.execSQL("DROP TABLE tmp");
// Step 2: Migrate LoyaltyCardDbIdsGroups to link to ID
db.execSQL("CREATE TEMPORARY TABLE tmp (" +
LoyaltyCardDbIdsGroups.cardID + " INTEGER," +
LoyaltyCardDbIdsGroups.groupID + " INTEGER," +
"primary key (" + LoyaltyCardDbIdsGroups.cardID + "," + LoyaltyCardDbIdsGroups.groupID + "))");
db.execSQL("INSERT INTO tmp (" +
LoyaltyCardDbIdsGroups.cardID + " ," +
LoyaltyCardDbIdsGroups.groupID + ")" +
" SELECT " +
LoyaltyCardDbGroups.NAME + " ," +
LoyaltyCardDbGroups.ORDER +
" FROM " + LoyaltyCardDbGroups.TABLE);
//////////
db.execSQL("CREATE TABLE " + LoyaltyCardDbIdsGroups.TABLE + "(" +
LoyaltyCardDbIdsGroups.cardID + " INTEGER," +
LoyaltyCardDbIdsGroups.groupID + " INTEGER," +
"primary key (" + LoyaltyCardDbIdsGroups.cardID + "," + LoyaltyCardDbIdsGroups.groupID + "))");
db.setTransactionSuccessful();
db.endTransaction();
}
return files;
}
private static ContentValues generateFTSContentValues(final int id, final String store, final String note) {
@@ -535,14 +581,14 @@ public class DBHelper extends SQLiteOpenHelper {
return (rowsUpdated == 1);
}
public static LoyaltyCard getLoyaltyCard(Context context, SQLiteDatabase database, final int id) {
public static LoyaltyCard getLoyaltyCard(SQLiteDatabase database, final int id) {
Cursor data = database.query(LoyaltyCardDbIds.TABLE, null, whereAttrs(LoyaltyCardDbIds.ID), withArgs(id), null, null, null);
LoyaltyCard card = null;
if (data.getCount() == 1) {
data.moveToFirst();
card = LoyaltyCard.fromCursor(context, data);
card = LoyaltyCard.toLoyaltyCard(data);
}
data.close();

View File

@@ -7,15 +7,17 @@ import android.database.sqlite.SQLiteDatabase;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import protect.card_locker.databinding.GroupLayoutBinding;
import protect.card_locker.preferences.Settings;
public class GroupCursorAdapter extends BaseCursorAdapter<GroupCursorAdapter.GroupListItemViewHolder> {
Settings mSettings;
public final Context mContext;
private final GroupAdapterListener mListener;
SQLiteDatabase mDatabase;
@@ -23,6 +25,7 @@ public class GroupCursorAdapter extends BaseCursorAdapter<GroupCursorAdapter.Gro
public GroupCursorAdapter(Context inputContext, Cursor inputCursor, GroupAdapterListener inputListener) {
super(inputCursor, DBHelper.LoyaltyCardDbGroups.ORDER);
setHasStableIds(true);
mSettings = new Settings(inputContext);
mContext = inputContext;
mListener = inputListener;
mDatabase = new DBHelper(inputContext).getReadableDatabase();
@@ -60,6 +63,8 @@ public class GroupCursorAdapter extends BaseCursorAdapter<GroupCursorAdapter.Gro
}
inputHolder.mCardCount.setText(cardCountText);
inputHolder.mName.setTextSize(mSettings.getFontSizeMax(mSettings.getMediumFont()));
inputHolder.mCardCount.setTextSize(mSettings.getFontSizeMax(mSettings.getSmallFont()));
applyClickEvents(inputHolder);
}
@@ -83,7 +88,7 @@ public class GroupCursorAdapter extends BaseCursorAdapter<GroupCursorAdapter.Gro
public static class GroupListItemViewHolder extends RecyclerView.ViewHolder {
public TextView mName, mCardCount;
public Button mMoveUp, mMoveDown, mEdit, mDelete;
public ImageButton mMoveUp, mMoveDown, mEdit, mDelete;
public GroupListItemViewHolder(GroupLayoutBinding groupLayoutBinding) {
super(groupLayoutBinding.getRoot());

View File

@@ -1,36 +1,38 @@
package protect.card_locker;
import android.Manifest;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
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 androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import protect.card_locker.async.TaskHandler;
import protect.card_locker.databinding.ImportExportActivityBinding;
import protect.card_locker.importexport.DataFormat;
@@ -81,21 +83,15 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
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();
try {
OutputStream writer = getContentResolver().openOutputStream(uri);
Log.e(TAG, "Starting file export with: " + result.toString());
startExport(writer, uri, exportPassword.toCharArray(), true);
} catch (IOException e) {
Log.e(TAG, "Failed to export file: " + result.toString(), e);
onExportComplete(new ImportExportResult(ImportExportResultType.GenericFailure, result.toString()), uri);
}
});
fileOpenLauncher = registerForActivityResult(new ActivityResultContracts.GetContent(), result -> {
if (result == null) {
@@ -130,19 +126,16 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
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);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.leftMargin = 50;
params.rightMargin = 50;
final EditText input = new EditText(ImportExportActivity.this);
input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
input.setLayoutParams(params);
input.setHint(R.string.exportPasswordHint);
textInputLayout.addView(input);
container.addView(textInputLayout);
container.addView(input);
builder.setView(container);
builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
exportPassword = input.getText().toString();
@@ -155,6 +148,7 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
});
builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> dialogInterface.cancel());
builder.show();
});
// Check that there is a file manager available
@@ -164,28 +158,17 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
// Check that there is an app that data can be imported from
Button importApplication = binding.importOptionApplicationButton;
importApplication.setOnClickListener(v -> chooseImportType(true, 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();
try {
InputStream reader = getContentResolver().openInputStream(uri);
Log.e(TAG, "Starting file import with: " + uri.toString());
startImport(reader, uri, importDataFormat, password, true);
} catch (IOException e) {
Log.e(TAG, "Failed to import file: " + uri.toString(), e);
onImportComplete(new ImportExportResult(ImportExportResultType.GenericFailure, e.toString()), uri, importDataFormat);
}
}
private void chooseImportType(boolean choosePicker,
@@ -337,21 +320,9 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
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);
final EditText input = new EditText(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.setView(input);
builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
openFileForImport(uri, input.getText().toString().toCharArray());

View File

@@ -6,7 +6,6 @@ import android.content.Context;
import android.content.DialogInterface;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import android.widget.Toast;
import java.io.IOException;
import java.io.InputStream;
@@ -91,16 +90,16 @@ public class ImportExportTask implements CompatCallable<ImportExportResult> {
progress = new ProgressDialog(activity);
progress.setTitle(doImport ? R.string.importing : R.string.exporting);
progress.setOnCancelListener(dialog -> cancel());
progress.setOnDismissListener(dialog -> cancel());
progress.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
ImportExportTask.this.stop();
}
});
progress.show();
}
private void cancel() {
ImportExportTask.this.stop();
}
protected ImportExportResult doInBackground(Void... nothing) {
final SQLiteDatabase database = new DBHelper(activity).getWritableDatabase();
ImportExportResult result;

View File

@@ -46,11 +46,8 @@ public class ImportURIHelper {
}
private boolean isImportUri(Uri uri) {
// Remove trailing slash added by some browsers (if it exists)
final String uriPath = uri.getPath().replaceAll("/$", "");
for (int i = 0; i < hosts.length; i++) {
if (uri.getHost().equals(hosts[i]) && uriPath.equals(paths[i])) {
if (uri.getHost().equals(hosts[i]) && uri.getPath().equals(paths[i])) {
return true;
}
}
@@ -125,30 +122,8 @@ public class ImportURIHelper {
headerColor = Integer.parseInt(unparsedHeaderColor);
}
return new LoyaltyCard(
-1,
store,
note,
validFrom,
expiry,
balance,
balanceType,
cardId,
barcodeId,
barcodeType,
headerColor,
0,
Utils.getUnixTime(),
100,
0,
null,
null,
null,
null,
null,
null
);
} catch (NumberFormatException | UnsupportedEncodingException | ArrayIndexOutOfBoundsException ex) {
return new LoyaltyCard(-1, store, note, validFrom, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headerColor, 0, Utils.getUnixTime(), 100, 0);
} catch (NullPointerException | NumberFormatException | UnsupportedEncodingException | ArrayIndexOutOfBoundsException ex) {
throw new InvalidObjectException("Not a valid import URI");
}
}

View File

@@ -1,623 +1,164 @@
package protect.card_locker;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
import java.math.BigDecimal;
import java.util.Currency;
import java.util.Date;
import java.util.List;
import java.util.Objects;
public class LoyaltyCard {
public int id;
public String store;
public String note;
import androidx.annotation.Nullable;
public class LoyaltyCard implements Parcelable {
public final int id;
public final String store;
public final String note;
public final Date validFrom;
public final Date expiry;
public final BigDecimal balance;
public final Currency balanceType;
public final String cardId;
@Nullable
public Date validFrom;
public final String barcodeId;
@Nullable
public Date expiry;
public BigDecimal balance;
public final CatimaBarcode barcodeType;
@Nullable
public Currency balanceType;
public String cardId;
@Nullable
public String barcodeId;
@Nullable
public CatimaBarcode barcodeType;
@Nullable
public Integer headerColor;
public int starStatus;
public long lastUsed;
public final Integer headerColor;
public final int starStatus;
public final int archiveStatus;
public final long lastUsed;
public int zoomLevel;
public int archiveStatus;
@Nullable
private Bitmap imageThumbnail;
@Nullable
private String imageThumbnailPath;
@Nullable
private Bitmap imageFront;
@Nullable
private String imageFrontPath;
@Nullable
private Bitmap imageBack;
@Nullable
private String imageBackPath;
public static final String BUNDLE_LOYALTY_CARD_ID = "loyaltyCardId";
public static final String BUNDLE_LOYALTY_CARD_STORE = "loyaltyCardStore";
public static final String BUNDLE_LOYALTY_CARD_NOTE = "loyaltyCardNote";
public static final String BUNDLE_LOYALTY_CARD_VALID_FROM = "loyaltyCardValidFrom";
public static final String BUNDLE_LOYALTY_CARD_EXPIRY = "loyaltyCardExpiry";
public static final String BUNDLE_LOYALTY_CARD_BALANCE = "loyaltyCardBalance";
public static final String BUNDLE_LOYALTY_CARD_BALANCE_TYPE = "loyaltyCardBalanceType";
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_HEADER_COLOR = "loyaltyCardHeaderColor";
public static final String BUNDLE_LOYALTY_CARD_STAR_STATUS = "loyaltyCardStarStatus";
public static final String BUNDLE_LOYALTY_CARD_LAST_USED = "loyaltyCardLastUsed";
public static final String BUNDLE_LOYALTY_CARD_ZOOM_LEVEL = "loyaltyCardZoomLevel";
public static final String BUNDLE_LOYALTY_CARD_ARCHIVE_STATUS = "loyaltyCardArchiveStatus";
public static final String BUNDLE_LOYALTY_CARD_IMAGE_THUMBNAIL = "loyaltyCardImageThumbnail";
public static final String BUNDLE_LOYALTY_CARD_IMAGE_FRONT = "loyaltyCardImageFront";
public static final String BUNDLE_LOYALTY_CARD_IMAGE_BACK = "loyaltyCardImageBack";
private static final String TEMP_IMAGE_THUMBNAIL_FILE_NAME = "loyaltyCardTempImageThumbnailFileName";
private static final String TEMP_IMAGE_FRONT_FILE_NAME = "loyaltyCardTempImageFrontFileName";
private static final String TEMP_IMAGE_BACK_FILE_NAME = "loyaltyCardTempImageBackFileName";
/**
* Create a loyalty card object with default values
*/
public LoyaltyCard() {
setId(-1);
setStore("");
setNote("");
setValidFrom(null);
setExpiry(null);
setBalance(new BigDecimal("0"));
setBalanceType(null);
setCardId("");
setBarcodeId(null);
setBarcodeType(null);
setHeaderColor(null);
setStarStatus(0);
setLastUsed(Utils.getUnixTime());
setZoomLevel(100);
setArchiveStatus(0);
setImageThumbnail(null, null);
setImageFront(null, null);
setImageBack(null, null);
}
/**
* Create a new loyalty card
*
* @param id
* @param store
* @param note
* @param validFrom
* @param expiry
* @param balance
* @param balanceType
* @param cardId
* @param barcodeId
* @param barcodeType
* @param headerColor
* @param starStatus
* @param lastUsed
* @param zoomLevel
* @param archiveStatus
*/
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,
public LoyaltyCard(final int id, final String store, final String note, final Date validFrom,
final Date expiry, final BigDecimal balance, final Currency balanceType,
final String cardId, @Nullable final String barcodeId,
@Nullable final CatimaBarcode barcodeType,
@Nullable final Integer headerColor, final int starStatus,
final long lastUsed, final int zoomLevel, final int archiveStatus,
@Nullable Bitmap imageThumbnail, @Nullable String imageThumbnailPath,
@Nullable Bitmap imageFront, @Nullable String imageFrontPath,
@Nullable Bitmap imageBack, @Nullable String imageBackPath) {
setId(id);
setStore(store);
setNote(note);
setValidFrom(validFrom);
setExpiry(expiry);
setBalance(balance);
setBalanceType(balanceType);
setCardId(cardId);
setBarcodeId(barcodeId);
setBarcodeType(barcodeType);
setHeaderColor(headerColor);
setStarStatus(starStatus);
setLastUsed(lastUsed);
setZoomLevel(zoomLevel);
setArchiveStatus(archiveStatus);
setImageThumbnail(imageThumbnail, imageThumbnailPath);
setImageFront(imageFront, imageFrontPath);
setImageBack(imageBack, imageBackPath);
}
@Nullable
public Bitmap getImageThumbnail(Context context) {
if (imageThumbnailPath != null) {
if (imageThumbnailPath.equals(TEMP_IMAGE_THUMBNAIL_FILE_NAME)) {
imageThumbnail = Utils.loadTempImage(context, imageThumbnailPath);
} else {
imageThumbnail = Utils.retrieveCardImage(context, imageThumbnailPath);
}
imageThumbnailPath = null;
}
if (imageThumbnail == null) {
return null;
}
return imageThumbnail.copy(imageThumbnail.getConfig(), imageThumbnail.isMutable());
}
@Nullable
public Bitmap getImageFront(Context context) {
if (imageFrontPath != null) {
if (imageFrontPath.equals(TEMP_IMAGE_FRONT_FILE_NAME)) {
imageFront = Utils.loadTempImage(context, imageFrontPath);
} else {
imageFront = Utils.retrieveCardImage(context, imageFrontPath);
}
imageFrontPath = null;
}
if (imageFront == null) {
return null;
}
return imageFront.copy(imageFront.getConfig(), imageFront.isMutable());
}
@Nullable
public Bitmap getImageBack(Context context) {
if (imageBackPath != null) {
if (imageBackPath.equals(TEMP_IMAGE_BACK_FILE_NAME)) {
imageBack = Utils.loadTempImage(context, imageBackPath);
} else {
imageBack = Utils.retrieveCardImage(context, imageBackPath);
}
imageBackPath = null;
}
if (imageBack == null) {
return null;
}
return imageBack.copy(imageBack.getConfig(), imageBack.isMutable());
}
public void setId(int id) {
final long lastUsed, final int zoomLevel, final int archiveStatus) {
this.id = id;
}
public void setStore(@NonNull String store) {
this.store = store;
}
public void setNote(@NonNull String note) {
this.note = note;
}
public void setValidFrom(@Nullable Date validFrom) {
this.validFrom = validFrom;
}
public void setExpiry(@Nullable Date expiry) {
this.expiry = expiry;
}
public void setBalance(@NonNull BigDecimal balance) {
this.balance = balance;
}
public void setBalanceType(@Nullable Currency balanceType) {
this.balanceType = balanceType;
}
public void setCardId(@NonNull String cardId) {
this.cardId = cardId;
}
public void setBarcodeId(@Nullable String barcodeId) {
this.barcodeId = barcodeId;
}
public void setBarcodeType(@Nullable CatimaBarcode barcodeType) {
this.barcodeType = barcodeType;
}
public void setHeaderColor(@Nullable Integer headerColor) {
this.headerColor = headerColor;
}
public void setStarStatus(int starStatus) {
if (starStatus != 0 && starStatus != 1) {
throw new IllegalArgumentException("starStatus must be 0 or 1");
}
this.starStatus = starStatus;
}
public void setLastUsed(long lastUsed) {
this.lastUsed = lastUsed;
}
public void setZoomLevel(int zoomLevel) {
if (zoomLevel < 0 || zoomLevel > 100) {
throw new IllegalArgumentException("zoomLevel must be in range 0-100");
}
this.zoomLevel = zoomLevel;
}
public void setArchiveStatus(int archiveStatus) {
if (archiveStatus != 0 && archiveStatus != 1) {
throw new IllegalArgumentException("archiveStatus must be 0 or 1");
}
this.archiveStatus = archiveStatus;
}
public void setImageThumbnail(@Nullable Bitmap imageThumbnail, @Nullable String imageThumbnailPath) {
if (imageThumbnail != null && imageThumbnailPath != null) {
throw new IllegalArgumentException("Cannot set both thumbnail and path");
}
this.imageThumbnailPath = imageThumbnailPath;
this.imageThumbnail = imageThumbnail != null ? imageThumbnail.copy(imageThumbnail.getConfig(), imageThumbnail.isMutable()) : null;
protected LoyaltyCard(Parcel in) {
id = in.readInt();
store = in.readString();
note = in.readString();
long tmpValidFrom = in.readLong();
validFrom = tmpValidFrom != -1 ? new Date(tmpValidFrom) : null;
long tmpExpiry = in.readLong();
expiry = tmpExpiry != -1 ? new Date(tmpExpiry) : null;
balance = (BigDecimal) in.readValue(BigDecimal.class.getClassLoader());
balanceType = (Currency) in.readValue(Currency.class.getClassLoader());
cardId = in.readString();
barcodeId = in.readString();
String tmpBarcodeType = in.readString();
barcodeType = !tmpBarcodeType.isEmpty() ? CatimaBarcode.fromName(tmpBarcodeType) : null;
int tmpHeaderColor = in.readInt();
headerColor = tmpHeaderColor != -1 ? tmpHeaderColor : null;
starStatus = in.readInt();
lastUsed = in.readLong();
zoomLevel = in.readInt();
archiveStatus = in.readInt();
}
public void setImageFront(@Nullable Bitmap imageFront, @Nullable String imageFrontPath) {
if (imageFront != null && imageFrontPath != null) {
throw new IllegalArgumentException("Cannot set both thumbnail and path");
}
this.imageFrontPath = imageFrontPath;
this.imageFront = imageFront != null ? imageFront.copy(imageFront.getConfig(), imageFront.isMutable()) : null;
}
public void setImageBack(@Nullable Bitmap imageBack, @Nullable String imageBackPath) {
if (imageBack != null && imageBackPath != null) {
throw new IllegalArgumentException("Cannot set both thumbnail and path");
}
this.imageBackPath = imageBackPath;
this.imageBack = imageBack != null ? imageBack.copy(imageBack.getConfig(), imageBack.isMutable()) : null;
}
@Nullable
public Bitmap getImageForImageLocationType(Context context, ImageLocationType imageLocationType) {
if (imageLocationType == ImageLocationType.icon) {
return getImageThumbnail(context);
} else if (imageLocationType == ImageLocationType.front) {
return getImageFront(context);
} else if (imageLocationType == ImageLocationType.back) {
return getImageBack(context);
}
throw new IllegalArgumentException("Unknown image location type");
}
public void updateFromBundle(@NonNull Bundle bundle, boolean requireFull) {
if (bundle.containsKey(BUNDLE_LOYALTY_CARD_ID)) {
setId(bundle.getInt(BUNDLE_LOYALTY_CARD_ID));
} else if (requireFull) {
throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_ID);
}
if (bundle.containsKey(BUNDLE_LOYALTY_CARD_STORE)) {
setStore(Objects.requireNonNull(bundle.getString(BUNDLE_LOYALTY_CARD_STORE)));
} else if (requireFull) {
throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_STORE);
}
if (bundle.containsKey(BUNDLE_LOYALTY_CARD_NOTE)) {
setNote(Objects.requireNonNull(bundle.getString(BUNDLE_LOYALTY_CARD_NOTE)));
} else if (requireFull) {
throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_NOTE);
}
if (bundle.containsKey(BUNDLE_LOYALTY_CARD_VALID_FROM)) {
long tmpValidFrom = bundle.getLong(BUNDLE_LOYALTY_CARD_VALID_FROM);
setValidFrom(tmpValidFrom > 0 ? new Date(tmpValidFrom) : null);
} else if (requireFull) {
throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_VALID_FROM);
}
if (bundle.containsKey(BUNDLE_LOYALTY_CARD_EXPIRY)) {
long tmpExpiry = bundle.getLong(BUNDLE_LOYALTY_CARD_EXPIRY);
setExpiry(tmpExpiry > 0 ? new Date(tmpExpiry) : null);
} else if (requireFull) {
throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_EXPIRY);
}
if (bundle.containsKey(BUNDLE_LOYALTY_CARD_BALANCE)) {
setBalance(new BigDecimal(bundle.getString(BUNDLE_LOYALTY_CARD_BALANCE)));
} else if (requireFull) {
throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_BALANCE);
}
if (bundle.containsKey(BUNDLE_LOYALTY_CARD_BALANCE_TYPE)) {
String tmpBalanceType = bundle.getString(BUNDLE_LOYALTY_CARD_BALANCE_TYPE);
setBalanceType(tmpBalanceType != null ? Currency.getInstance(tmpBalanceType) : null);
} else if (requireFull) {
throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_BALANCE_TYPE);
}
if (bundle.containsKey(BUNDLE_LOYALTY_CARD_CARD_ID)) {
setCardId(Objects.requireNonNull(bundle.getString(BUNDLE_LOYALTY_CARD_CARD_ID)));
} else if (requireFull) {
throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_CARD_ID);
}
if (bundle.containsKey(BUNDLE_LOYALTY_CARD_BARCODE_ID)) {
setBarcodeId(bundle.getString(BUNDLE_LOYALTY_CARD_BARCODE_ID));
} else if (requireFull) {
throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_BARCODE_ID);
}
if (bundle.containsKey(BUNDLE_LOYALTY_CARD_BARCODE_TYPE)) {
String tmpBarcodeType = bundle.getString(BUNDLE_LOYALTY_CARD_BARCODE_TYPE);
setBarcodeType(tmpBarcodeType != null ? CatimaBarcode.fromName(tmpBarcodeType) : null);
} else if (requireFull) {
throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_BARCODE_TYPE);
}
if (bundle.containsKey(BUNDLE_LOYALTY_CARD_HEADER_COLOR)) {
int tmpHeaderColor = bundle.getInt(BUNDLE_LOYALTY_CARD_HEADER_COLOR);
setHeaderColor(tmpHeaderColor != -1 ? tmpHeaderColor : null);
} else if (requireFull) {
throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_HEADER_COLOR);
}
if (bundle.containsKey(BUNDLE_LOYALTY_CARD_STAR_STATUS)) {
setStarStatus(bundle.getInt(BUNDLE_LOYALTY_CARD_STAR_STATUS));
} else if (requireFull) {
throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_STAR_STATUS);
}
if (bundle.containsKey(BUNDLE_LOYALTY_CARD_LAST_USED)) {
setLastUsed(bundle.getLong(BUNDLE_LOYALTY_CARD_LAST_USED));
} else if (requireFull) {
throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_LAST_USED);
}
if (bundle.containsKey(BUNDLE_LOYALTY_CARD_ZOOM_LEVEL)) {
setZoomLevel(bundle.getInt(BUNDLE_LOYALTY_CARD_ZOOM_LEVEL));
} else if (requireFull) {
throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_ZOOM_LEVEL);
}
if (bundle.containsKey(BUNDLE_LOYALTY_CARD_ARCHIVE_STATUS)) {
setArchiveStatus(bundle.getInt(BUNDLE_LOYALTY_CARD_ARCHIVE_STATUS));
} else if (requireFull) {
throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_ARCHIVE_STATUS);
}
if (bundle.containsKey(BUNDLE_LOYALTY_CARD_IMAGE_THUMBNAIL)) {
setImageThumbnail(null, bundle.getString(BUNDLE_LOYALTY_CARD_IMAGE_THUMBNAIL));
} else if (requireFull) {
throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_IMAGE_THUMBNAIL);
}
if (bundle.containsKey(BUNDLE_LOYALTY_CARD_IMAGE_FRONT)) {
setImageFront(null, bundle.getString(BUNDLE_LOYALTY_CARD_IMAGE_FRONT));
} else if (requireFull) {
throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_IMAGE_FRONT);
}
if (bundle.containsKey(BUNDLE_LOYALTY_CARD_IMAGE_BACK)) {
setImageBack(null, bundle.getString(BUNDLE_LOYALTY_CARD_IMAGE_BACK));
} else if (requireFull) {
throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_IMAGE_BACK);
}
}
public Bundle toBundle(Context context, List<String> exportLimit) {
boolean exportIsLimited = !exportLimit.isEmpty();
Bundle bundle = new Bundle();
if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_ID)) {
bundle.putInt(BUNDLE_LOYALTY_CARD_ID, id);
}
if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_STORE)) {
bundle.putString(BUNDLE_LOYALTY_CARD_STORE, store);
}
if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_NOTE)) {
bundle.putString(BUNDLE_LOYALTY_CARD_NOTE, note);
}
if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_VALID_FROM)) {
bundle.putLong(BUNDLE_LOYALTY_CARD_VALID_FROM, validFrom != null ? validFrom.getTime() : -1);
}
if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_EXPIRY)) {
bundle.putLong(BUNDLE_LOYALTY_CARD_EXPIRY, expiry != null ? expiry.getTime() : -1);
}
if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_BALANCE)) {
bundle.putString(BUNDLE_LOYALTY_CARD_BALANCE, balance.toString());
}
if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_BALANCE_TYPE)) {
bundle.putString(BUNDLE_LOYALTY_CARD_BALANCE_TYPE, balanceType != null ? balanceType.toString() : null);
}
if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_CARD_ID)) {
bundle.putString(BUNDLE_LOYALTY_CARD_CARD_ID, cardId);
}
if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_BARCODE_ID)) {
bundle.putString(BUNDLE_LOYALTY_CARD_BARCODE_ID, barcodeId);
}
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_HEADER_COLOR)) {
bundle.putInt(BUNDLE_LOYALTY_CARD_HEADER_COLOR, headerColor != null ? headerColor : -1);
}
if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_STAR_STATUS)) {
bundle.putInt(BUNDLE_LOYALTY_CARD_STAR_STATUS, starStatus);
}
if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_LAST_USED)) {
bundle.putLong(BUNDLE_LOYALTY_CARD_LAST_USED, lastUsed);
}
if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_ZOOM_LEVEL)) {
bundle.putInt(BUNDLE_LOYALTY_CARD_ZOOM_LEVEL, zoomLevel);
}
if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_ARCHIVE_STATUS)) {
bundle.putInt(BUNDLE_LOYALTY_CARD_ARCHIVE_STATUS, archiveStatus);
}
// There is an (undocumented) size limit to bundles of around 2MB(?), when going over it you will experience a random crash
// So, instead of storing the bitmaps directly, we write the bitmap to a temp file and store the path
if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_IMAGE_THUMBNAIL)) {
Bitmap thumbnail = getImageThumbnail(context);
if (thumbnail != null) {
Utils.saveTempImage(context, thumbnail, TEMP_IMAGE_THUMBNAIL_FILE_NAME, Bitmap.CompressFormat.PNG);
bundle.putString(BUNDLE_LOYALTY_CARD_IMAGE_THUMBNAIL, TEMP_IMAGE_THUMBNAIL_FILE_NAME);
} else {
bundle.putString(BUNDLE_LOYALTY_CARD_IMAGE_THUMBNAIL, null);
}
}
if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_IMAGE_FRONT)) {
Bitmap front = getImageFront(context);
if (front != null) {
Utils.saveTempImage(context, front, TEMP_IMAGE_FRONT_FILE_NAME, Bitmap.CompressFormat.PNG);
bundle.putString(BUNDLE_LOYALTY_CARD_IMAGE_FRONT, TEMP_IMAGE_FRONT_FILE_NAME);
} else {
bundle.putString(BUNDLE_LOYALTY_CARD_IMAGE_FRONT, null);
}
}
if (!exportIsLimited || exportLimit.contains(BUNDLE_LOYALTY_CARD_IMAGE_BACK)) {
Bitmap back = getImageBack(context);
if (back != null) {
Utils.saveTempImage(context, back, TEMP_IMAGE_BACK_FILE_NAME, Bitmap.CompressFormat.PNG);
bundle.putString(BUNDLE_LOYALTY_CARD_IMAGE_BACK, TEMP_IMAGE_BACK_FILE_NAME);
} else {
bundle.putString(BUNDLE_LOYALTY_CARD_IMAGE_BACK, null);
}
}
return bundle;
}
public static LoyaltyCard fromCursor(Context context, Cursor cursor) {
// id
int id = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ID));
// store
String store = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.STORE));
// note
String note = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.NOTE));
// validFrom
long validFromLong = cursor.getLong(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.VALID_FROM));
Date validFrom = validFromLong > 0 ? new Date(validFromLong) : null;
// expiry
long expiryLong = cursor.getLong(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.EXPIRY));
Date expiry = expiryLong > 0 ? new Date(expiryLong) : null;
// balance
BigDecimal balance = new BigDecimal(cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BALANCE)));
// balanceType
int balanceTypeColumn = cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BALANCE_TYPE);
Currency balanceType = !cursor.isNull(balanceTypeColumn) ? Currency.getInstance(cursor.getString(balanceTypeColumn)) : null;
// cardId
String cardId = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.CARD_ID));
// barcodeId
int barcodeIdColumn = cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BARCODE_ID);
String barcodeId = !cursor.isNull(barcodeIdColumn) ? cursor.getString(barcodeIdColumn) : null;
// barcodeType
int barcodeTypeColumn = cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BARCODE_TYPE);
CatimaBarcode barcodeType = !cursor.isNull(barcodeTypeColumn) ? CatimaBarcode.fromName(cursor.getString(barcodeTypeColumn)) : null;
// headerColor
int headerColorColumn = cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.HEADER_COLOR);
Integer headerColor = !cursor.isNull(headerColorColumn) ? cursor.getInt(headerColorColumn) : null;
// starStatus
int starStatus = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.STAR_STATUS));
// lastUsed
long lastUsed = cursor.getLong(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.LAST_USED));
// zoomLevel
int zoomLevel = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ZOOM_LEVEL));
// archiveStatus
int archiveStatus = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ARCHIVE_STATUS));
return new LoyaltyCard(
id,
store,
note,
validFrom,
expiry,
balance,
balanceType,
cardId,
barcodeId,
barcodeType,
headerColor,
starStatus,
lastUsed,
zoomLevel,
archiveStatus,
null,
Utils.getCardImageFileName(id, ImageLocationType.icon),
null,
Utils.getCardImageFileName(id, ImageLocationType.front),
null,
Utils.getCardImageFileName(id, ImageLocationType.back)
);
}
public static boolean isDuplicate(Context context, final LoyaltyCard a, final LoyaltyCard b) {
// Note: Bitmap comparing is slow, be careful when calling this method
// Skip lastUsed & zoomLevel
return a.id == b.id && // non-nullable int
a.store.equals(b.store) && // non-nullable String
a.note.equals(b.note) && // non-nullable String
Utils.equals(a.validFrom, b.validFrom) && // nullable Date
Utils.equals(a.expiry, b.expiry) && // nullable Date
a.balance.equals(b.balance) && // non-nullable BigDecimal
Utils.equals(a.balanceType, b.balanceType) && // nullable Currency
a.cardId.equals(b.cardId) && // non-nullable String
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.headerColor, b.headerColor) && // nullable Integer
a.starStatus == b.starStatus && // non-nullable int
a.archiveStatus == b.archiveStatus && // non-nullable int
nullableBitmapsEqual(a.getImageThumbnail(context), b.getImageThumbnail(context)) && // nullable Bitmap
nullableBitmapsEqual(a.getImageFront(context), b.getImageFront(context)) && // nullable Bitmap
nullableBitmapsEqual(a.getImageBack(context), b.getImageBack(context)); // nullable Bitmap
}
public static boolean nullableBitmapsEqual(@Nullable Bitmap a, @Nullable Bitmap b) {
if (a == null && b == null) {
return true;
}
if (a != null && b != null) {
return a.sameAs(b);
}
// One is null and the other isn't, so it's not equal
return false;
}
@NonNull
@Override
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"
+ " headerColor=%s,%n starStatus=%s,%n lastUsed=%s,%n zoomLevel=%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,
this.store,
this.note,
this.validFrom,
this.expiry,
this.balance,
this.balanceType,
this.cardId,
this.barcodeId,
this.barcodeType != null ? this.barcodeType.format() : null,
this.headerColor,
this.starStatus,
this.lastUsed,
this.zoomLevel,
this.archiveStatus,
this.imageThumbnail,
this.imageThumbnailPath,
this.imageFront,
this.imageFrontPath,
this.imageBack,
this.imageBackPath
);
public void writeToParcel(Parcel parcel, int i) {
parcel.writeInt(id);
parcel.writeString(store);
parcel.writeString(note);
parcel.writeLong(validFrom != null ? validFrom.getTime() : -1);
parcel.writeLong(expiry != null ? expiry.getTime() : -1);
parcel.writeValue(balance);
parcel.writeValue(balanceType);
parcel.writeString(cardId);
parcel.writeString(barcodeId);
parcel.writeString(barcodeType != null ? barcodeType.name() : "");
parcel.writeInt(headerColor != null ? headerColor : -1);
parcel.writeInt(starStatus);
parcel.writeLong(lastUsed);
parcel.writeInt(zoomLevel);
parcel.writeInt(archiveStatus);
}
public static LoyaltyCard toLoyaltyCard(Cursor cursor) {
int id = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ID));
String store = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.STORE));
String note = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.NOTE));
long validFromLong = cursor.getLong(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.VALID_FROM));
long expiryLong = cursor.getLong(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.EXPIRY));
BigDecimal balance = new BigDecimal(cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BALANCE)));
String cardId = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.CARD_ID));
String barcodeId = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BARCODE_ID));
int starred = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.STAR_STATUS));
long lastUsed = cursor.getLong(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.LAST_USED));
int zoomLevel = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ZOOM_LEVEL));
int archived = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ARCHIVE_STATUS));
int barcodeTypeColumn = cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BARCODE_TYPE);
int balanceTypeColumn = cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.BALANCE_TYPE);
int headerColorColumn = cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.HEADER_COLOR);
CatimaBarcode barcodeType = null;
Currency balanceType = null;
Date validFrom = null;
Date expiry = null;
Integer headerColor = null;
if (cursor.isNull(barcodeTypeColumn) == false) {
barcodeType = CatimaBarcode.fromName(cursor.getString(barcodeTypeColumn));
}
if (cursor.isNull(balanceTypeColumn) == false) {
balanceType = Currency.getInstance(cursor.getString(balanceTypeColumn));
}
if (validFromLong > 0) {
validFrom = new Date(validFromLong);
}
if (expiryLong > 0) {
expiry = new Date(expiryLong);
}
if (cursor.isNull(headerColorColumn) == false) {
headerColor = cursor.getInt(headerColorColumn);
}
return new LoyaltyCard(id, store, note, validFrom, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headerColor, starred, lastUsed, zoomLevel, archived);
}
@Override
public int describeContents() {
return id;
}
public static final Creator<LoyaltyCard> CREATOR = new Creator<LoyaltyCard>() {
@Override
public LoyaltyCard createFromParcel(Parcel in) {
return new LoyaltyCard(in);
}
@Override
public LoyaltyCard[] newArray(int size) {
return new LoyaltyCard[size];
}
};
}

View File

@@ -1,6 +1,7 @@
package protect.card_locker;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
@@ -15,13 +16,6 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.BlendModeColorFilterCompat;
import androidx.core.graphics.BlendModeCompat;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.card.MaterialCardView;
import com.google.android.material.color.MaterialColors;
@@ -29,42 +23,66 @@ import java.math.BigDecimal;
import java.text.DateFormat;
import java.util.ArrayList;
import androidx.annotation.NonNull;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.BlendModeColorFilterCompat;
import androidx.core.graphics.BlendModeCompat;
import androidx.recyclerview.widget.RecyclerView;
import protect.card_locker.databinding.LoyaltyCardLayoutBinding;
import protect.card_locker.preferences.Settings;
public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCursorAdapter.LoyaltyCardListItemViewHolder> {
private int mCurrentSelectedIndex = -1;
Settings mSettings;
boolean mDarkModeEnabled;
public final Context mContext;
private final CardAdapterListener mListener;
private final LoyaltyCardListDisplayOptionsManager mLoyaltyCardListDisplayOptions;
protected SparseBooleanArray mSelectedItems;
protected SparseBooleanArray mAnimationItemsIndex;
private boolean mReverseAllAnimations = false;
private boolean mShowDetails;
public LoyaltyCardCursorAdapter(Context inputContext, Cursor inputCursor, CardAdapterListener inputListener, Runnable inputSwapCursorCallback) {
public LoyaltyCardCursorAdapter(Context inputContext, Cursor inputCursor, CardAdapterListener inputListener) {
super(inputCursor, DBHelper.LoyaltyCardDbIds.ID);
setHasStableIds(true);
mSettings = new Settings(inputContext);
mContext = inputContext;
mListener = inputListener;
Runnable refreshCardsCallback = () -> notifyDataSetChanged();
mLoyaltyCardListDisplayOptions = new LoyaltyCardListDisplayOptionsManager(mContext, refreshCardsCallback, inputSwapCursorCallback);
mSelectedItems = new SparseBooleanArray();
mAnimationItemsIndex = new SparseBooleanArray();
mDarkModeEnabled = Utils.isDarkModeEnabled(inputContext);
refreshState();
swapCursor(inputCursor);
}
public void showDisplayOptionsDialog() {
mLoyaltyCardListDisplayOptions.showDisplayOptionsDialog();
public void refreshState() {
// Retrieve user details preference
SharedPreferences cardDetailsPref = mContext.getSharedPreferences(
mContext.getString(R.string.sharedpreference_card_details),
Context.MODE_PRIVATE);
mShowDetails = cardDetailsPref.getBoolean(mContext.getString(R.string.sharedpreference_card_details_show), true);
}
public boolean showingArchivedCards() {
return mLoyaltyCardListDisplayOptions.showingArchivedCards();
public void showDetails(boolean show) {
mShowDetails = show;
notifyDataSetChanged();
// Store in Shared Preference to restore next adapter launch
SharedPreferences cardDetailsPref = mContext.getSharedPreferences(
mContext.getString(R.string.sharedpreference_card_details),
Context.MODE_PRIVATE);
SharedPreferences.Editor cardDetailsPrefEditor = cardDetailsPref.edit();
cardDetailsPrefEditor.putBoolean(mContext.getString(R.string.sharedpreference_card_details_show), show);
cardDetailsPrefEditor.apply();
}
public boolean showingDetails() {
return mShowDetails;
}
@NonNull
@@ -80,53 +98,52 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
public LoyaltyCard getCard(int position) {
mCursor.moveToPosition(position);
return LoyaltyCard.fromCursor(mContext, mCursor);
return LoyaltyCard.toLoyaltyCard(mCursor);
}
public void onBindViewHolder(LoyaltyCardListItemViewHolder inputHolder, Cursor inputCursor) {
// Invisible until we want to show something more
boolean showDivider = false;
inputHolder.mDivider.setVisibility(View.GONE);
LoyaltyCard loyaltyCard = LoyaltyCard.fromCursor(mContext, inputCursor);
Bitmap icon = loyaltyCard.getImageThumbnail(mContext);
LoyaltyCard loyaltyCard = LoyaltyCard.toLoyaltyCard(inputCursor);
if (mLoyaltyCardListDisplayOptions.showingNameBelowThumbnail() && icon != null) {
showDivider = true;
inputHolder.setStoreField(loyaltyCard.store);
} else {
inputHolder.setStoreField(null);
}
if (mLoyaltyCardListDisplayOptions.showingNote() && !loyaltyCard.note.isEmpty()) {
showDivider = true;
inputHolder.setStoreField(loyaltyCard.store);
if (mShowDetails && !loyaltyCard.note.isEmpty()) {
inputHolder.setNoteField(loyaltyCard.note);
} else {
inputHolder.setNoteField(null);
}
if (mLoyaltyCardListDisplayOptions.showingBalance() && !loyaltyCard.balance.equals(new BigDecimal("0"))) {
inputHolder.setExtraField(inputHolder.mBalanceField, Utils.formatBalance(mContext, loyaltyCard.balance, loyaltyCard.balanceType), null, showDivider);
if (mShowDetails && !loyaltyCard.balance.equals(new BigDecimal("0"))) {
inputHolder.setExtraField(inputHolder.mBalanceField, Utils.formatBalance(mContext, loyaltyCard.balance, loyaltyCard.balanceType), null);
} else {
inputHolder.setExtraField(inputHolder.mBalanceField, null, null, false);
inputHolder.setExtraField(inputHolder.mBalanceField, null, null);
}
if (mLoyaltyCardListDisplayOptions.showingValidity() && loyaltyCard.validFrom != null) {
inputHolder.setExtraField(inputHolder.mValidFromField, DateFormat.getDateInstance(DateFormat.MEDIUM).format(loyaltyCard.validFrom), Utils.isNotYetValid(loyaltyCard.validFrom) ? Color.RED : null, showDivider);
if (mShowDetails && loyaltyCard.validFrom != null) {
inputHolder.setExtraField(inputHolder.mValidFromField, DateFormat.getDateInstance(DateFormat.LONG).format(loyaltyCard.validFrom), Utils.isNotYetValid(loyaltyCard.validFrom) ? Color.RED : null);
} else {
inputHolder.setExtraField(inputHolder.mValidFromField, null, null, false);
inputHolder.setExtraField(inputHolder.mValidFromField, null, null);
}
if (mLoyaltyCardListDisplayOptions.showingValidity() && loyaltyCard.expiry != null) {
inputHolder.setExtraField(inputHolder.mExpiryField, DateFormat.getDateInstance(DateFormat.MEDIUM).format(loyaltyCard.expiry), Utils.hasExpired(loyaltyCard.expiry) ? Color.RED : null, showDivider);
if (mShowDetails && loyaltyCard.expiry != null) {
inputHolder.setExtraField(inputHolder.mExpiryField, DateFormat.getDateInstance(DateFormat.LONG).format(loyaltyCard.expiry), Utils.hasExpired(loyaltyCard.expiry) ? Color.RED : null);
} else {
inputHolder.setExtraField(inputHolder.mExpiryField, null, null, false);
inputHolder.setExtraField(inputHolder.mExpiryField, null, null);
}
inputHolder.mCardIcon.setContentDescription(loyaltyCard.store);
Utils.setIconOrTextWithBackground(mContext, loyaltyCard, icon, inputHolder.mCardIcon, inputHolder.mCardText, new Settings(mContext).getPreferredColumnCount());
setHeaderHeight(inputHolder, mShowDetails);
Bitmap cardIcon = Utils.retrieveCardImage(mContext, loyaltyCard.id, ImageLocationType.icon);
if (cardIcon != null) {
inputHolder.mCardIcon.setImageBitmap(cardIcon);
inputHolder.mCardIcon.setScaleType(ImageView.ScaleType.CENTER_CROP);
} else {
inputHolder.mCardIcon.setImageBitmap(Utils.generateIcon(mContext, loyaltyCard.store, loyaltyCard.headerColor).getLetterTile());
inputHolder.mCardIcon.setScaleType(ImageView.ScaleType.FIT_CENTER);
}
inputHolder.setIconBackgroundColor(loyaltyCard.headerColor != null ? loyaltyCard.headerColor : R.attr.colorPrimary);
inputHolder.toggleCardStateIcon(loyaltyCard.starStatus != 0, loyaltyCard.archiveStatus != 0);
inputHolder.toggleCardStateIcon(loyaltyCard.starStatus != 0, loyaltyCard.archiveStatus != 0, itemSelected(inputCursor.getPosition()));
inputHolder.itemView.setActivated(mSelectedItems.get(inputCursor.getPosition(), false));
applyIconAnimation(inputHolder, inputCursor.getPosition());
@@ -136,6 +153,19 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
inputHolder.mRow.requestLayout();
}
private void setHeaderHeight(LoyaltyCardListItemViewHolder inputHolder, boolean expanded) {
int iconHeight;
if (expanded) {
iconHeight = ViewGroup.LayoutParams.MATCH_PARENT;
} else {
iconHeight = (int) mContext.getResources().getDimension(R.dimen.cardThumbnailSize);
}
inputHolder.mIconLayout.getLayoutParams().height = expanded ? 0 : iconHeight;
inputHolder.mCardIcon.getLayoutParams().height = iconHeight;
inputHolder.mTickIcon.getLayoutParams().height = iconHeight;
}
private void applyClickEvents(LoyaltyCardListItemViewHolder inputHolder, final int inputPosition) {
inputHolder.mRow.setOnClickListener(inputView -> mListener.onRowClicked(inputPosition));
@@ -193,7 +223,7 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
int i;
for (i = 0; i < mSelectedItems.size(); i++) {
mCursor.moveToPosition(mSelectedItems.keyAt(i));
result.add(LoyaltyCard.fromCursor(mContext, mCursor));
result.add(LoyaltyCard.toLoyaltyCard(mCursor));
}
return result;
@@ -211,12 +241,16 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
public class LoyaltyCardListItemViewHolder extends RecyclerView.ViewHolder {
public TextView mCardText, mStoreField, mNoteField, mBalanceField, mValidFromField, mExpiryField;
public ImageView mCardIcon, mTickIcon;
public MaterialCardView mRow;
public TextView mStoreField, mNoteField, mBalanceField, mValidFromField, mExpiryField;
public ImageView mCardIcon, mStarBackground, mStarBorder, mTickIcon, mArchivedBackground;
public MaterialCardView mRow, mIconLayout;
public ConstraintLayout mStar, mArchived;
public View mDivider;
private int mIconBackgroundColor;
protected LoyaltyCardListItemViewHolder(LoyaltyCardLayoutBinding loyaltyCardLayoutBinding, CardAdapterListener inputListener) {
super(loyaltyCardLayoutBinding.getRoot());
View inputView = loyaltyCardLayoutBinding.getRoot();
@@ -227,10 +261,13 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
mBalanceField = loyaltyCardLayoutBinding.balance;
mValidFromField = loyaltyCardLayoutBinding.validFrom;
mExpiryField = loyaltyCardLayoutBinding.expiry;
mIconLayout = loyaltyCardLayoutBinding.iconLayout;
mCardIcon = loyaltyCardLayoutBinding.thumbnail;
mCardText = loyaltyCardLayoutBinding.thumbnailText;
mStar = loyaltyCardLayoutBinding.star;
mStarBackground = loyaltyCardLayoutBinding.starBackground;
mStarBorder = loyaltyCardLayoutBinding.starBorder;
mArchived = loyaltyCardLayoutBinding.archivedIcon;
mArchivedBackground = loyaltyCardLayoutBinding.archiveBackground;
mTickIcon = loyaltyCardLayoutBinding.selectedThumbnail;
inputView.setOnLongClickListener(view -> {
inputListener.onRowClicked(getAdapterPosition());
@@ -239,7 +276,7 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
});
}
private void setExtraField(TextView field, String text, Integer color, boolean showDivider) {
private void setExtraField(TextView field, String text, Integer color) {
// If text is null, hide the field
// If iconColor is null, use the default text and icon color based on theme
if (text == null) {
@@ -248,18 +285,20 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
return;
}
// Shown when there is a name and/or note and at least 1 extra field
if (showDivider) {
mDivider.setVisibility(View.VISIBLE);
}
int size = mSettings.getFontSizeMax(mSettings.getSmallFont());
field.setText(text);
field.setTextColor(color != null ? color : MaterialColors.getColor(mContext, com.google.android.material.R.attr.colorSecondary, ContextCompat.getColor(mContext, mDarkModeEnabled ? R.color.md_theme_dark_secondary : R.color.md_theme_light_secondary)));
field.setVisibility(View.VISIBLE);
field.setText(text);
field.setTextSize(size);
field.setTextColor(color != null ? color : MaterialColors.getColor(mContext, R.attr.colorSecondary, ContextCompat.getColor(mContext, mDarkModeEnabled ? R.color.md_theme_dark_secondary : R.color.md_theme_light_secondary)));
int drawableSize = dpToPx((size * 24) / 14, mContext);
mDivider.setVisibility(View.VISIBLE);
field.setVisibility(View.VISIBLE);
Drawable icon = field.getCompoundDrawables()[0];
if (icon != null) {
icon.mutate();
icon.setBounds(0, 0, drawableSize, drawableSize);
field.setCompoundDrawablesRelative(icon, null, null, null);
if (color != null) {
@@ -273,12 +312,8 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
}
public void setStoreField(String text) {
if (text == null) {
mStoreField.setVisibility(View.GONE);
} else {
mStoreField.setVisibility(View.VISIBLE);
mStoreField.setText(text);
}
mStoreField.setText(text);
mStoreField.setTextSize(mSettings.getFontSizeMax(mSettings.getMediumFont()));
mStoreField.requestLayout();
}
@@ -288,11 +323,36 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
} else {
mNoteField.setVisibility(View.VISIBLE);
mNoteField.setText(text);
mNoteField.setTextSize(mSettings.getFontSizeMax(mSettings.getSmallFont()));
}
mNoteField.requestLayout();
}
public void toggleCardStateIcon(boolean enableStar, boolean enableArchive) {
public void toggleCardStateIcon(boolean enableStar, boolean enableArchive, boolean colorByTheme) {
/* the below code does not work in android 5! hence the change of drawable instead
boolean needDarkForeground = Utils.needsDarkForeground(mIconBackgroundColor);
Drawable borderDrawable = mStarBorder.getDrawable().mutate();
Drawable backgroundDrawable = mStarBackground.getDrawable().mutate();
DrawableCompat.setTint(borderDrawable, needsDarkForeground ? Color.BLACK : Color.WHITE);
DrawableCompat.setTint(backgroundDrawable, needsDarkForeground ? Color.BLACK : Color.WHITE);
mStarBorder.setImageDrawable(borderDrawable);
mStarBackground.setImageDrawable(backgroundDrawable);
*/
boolean dark = Utils.needsDarkForeground(mIconBackgroundColor);
if (colorByTheme) {
dark = !mDarkModeEnabled;
}
if (dark) {
mStarBorder.setImageResource(R.drawable.ic_unstarred_white);
mStarBackground.setImageResource(R.drawable.ic_starred_black);
mArchivedBackground.setImageResource(R.drawable.ic_baseline_archive_24_black);
} else {
mStarBorder.setImageResource(R.drawable.ic_unstarred_black);
mStarBackground.setImageResource(R.drawable.ic_starred_white);
mArchivedBackground.setImageResource(R.drawable.ic_baseline_archive_24);
}
if (enableStar) {
mStar.setVisibility(View.VISIBLE);
} else{
@@ -304,6 +364,22 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
} else{
mArchived.setVisibility(View.GONE);
}
mStarBorder.invalidate();
mStarBackground.invalidate();
mArchivedBackground.invalidate();
}
public void setIconBackgroundColor(int color) {
mIconBackgroundColor = color;
mCardIcon.setBackgroundColor(color);
}
}
public int dpToPx(int dp, Context mContext) {
Resources r = mContext.getResources();
int px = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics());
return px;
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,176 +0,0 @@
package protect.card_locker;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public class LoyaltyCardListDisplayOptionsManager {
public static class LoyaltyCardDisplayOption {
public String name;
public boolean value;
public Consumer<Boolean> callback;
LoyaltyCardDisplayOption(String name, boolean value, Consumer<Boolean> callback) {
this.name = name;
this.value = value;
this.callback = callback;
}
}
public final Context mContext;
private final Runnable mRefreshCardsCallback;
private final Runnable mSwapCursorCallback;
protected SharedPreferences mCardDetailsPref;
private boolean mShowNameBelowThumbnail;
private boolean mShowNote;
private boolean mShowBalance;
private boolean mShowValidity;
private boolean mShowArchivedCards;
public LoyaltyCardListDisplayOptionsManager(Context context, @NonNull Runnable refreshCardsCallback, @Nullable Runnable swapCursorCallback) {
mContext = context;
mRefreshCardsCallback = refreshCardsCallback;
mSwapCursorCallback = swapCursorCallback;
// Retrieve user details preference
mCardDetailsPref = mContext.getSharedPreferences(
mContext.getString(R.string.sharedpreference_card_details),
Context.MODE_PRIVATE);
mShowNameBelowThumbnail = mCardDetailsPref.getBoolean(mContext.getString(R.string.sharedpreference_card_details_show_name_below_thumbnail), false);
mShowNote = mCardDetailsPref.getBoolean(mContext.getString(R.string.sharedpreference_card_details_show_note), true);
mShowBalance = mCardDetailsPref.getBoolean(mContext.getString(R.string.sharedpreference_card_details_show_balance), true);
mShowValidity = mCardDetailsPref.getBoolean(mContext.getString(R.string.sharedpreference_card_details_show_validity), true);
mShowArchivedCards = mCardDetailsPref.getBoolean(mContext.getString(R.string.sharedpreference_card_details_show_archived_cards), true);
}
void saveDetailState(int stateId, boolean value) {
SharedPreferences.Editor cardDetailsPrefEditor = mCardDetailsPref.edit();
cardDetailsPrefEditor.putBoolean(mContext.getString(stateId), value);
cardDetailsPrefEditor.apply();
}
public void showNameBelowThumbnail(boolean show) {
mShowNameBelowThumbnail = show;
mRefreshCardsCallback.run();
saveDetailState(R.string.sharedpreference_card_details_show_name_below_thumbnail, show);
}
public boolean showingNameBelowThumbnail() {
return mShowNameBelowThumbnail;
}
public void showNote(boolean show) {
mShowNote = show;
mRefreshCardsCallback.run();
saveDetailState(R.string.sharedpreference_card_details_show_note, show);
}
public boolean showingNote() {
return mShowNote;
}
public void showBalance(boolean show) {
mShowBalance = show;
mRefreshCardsCallback.run();
saveDetailState(R.string.sharedpreference_card_details_show_balance, show);
}
public boolean showingBalance() {
return mShowBalance;
}
public void showValidity(boolean show) {
mShowValidity = show;
mRefreshCardsCallback.run();
saveDetailState(R.string.sharedpreference_card_details_show_validity, show);
}
public boolean showingValidity() {
return mShowValidity;
}
public void showArchivedCards(boolean show) {
if (mSwapCursorCallback == null) {
throw new IllegalStateException("No swap cursor callback is available, can not manage archive state");
}
mShowArchivedCards = show;
mSwapCursorCallback.run();
saveDetailState(R.string.sharedpreference_card_details_show_archived_cards, show);
}
public boolean showingArchivedCards() {
if (mSwapCursorCallback == null) {
throw new IllegalStateException("No swap cursor callback is available, can not manage archive state");
}
return mShowArchivedCards;
}
public void showDisplayOptionsDialog() {
List<LoyaltyCardDisplayOption> displayOptions = new ArrayList<>();
displayOptions.add(new LoyaltyCardDisplayOption(
mContext.getString(R.string.show_name_below_image_thumbnail),
showingNameBelowThumbnail(),
this::showNameBelowThumbnail
));
displayOptions.add(new LoyaltyCardDisplayOption(
mContext.getString(R.string.show_note),
showingNote(),
this::showNote
));
displayOptions.add(new LoyaltyCardDisplayOption(
mContext.getString(R.string.show_balance),
showingBalance(),
this::showBalance
));
displayOptions.add(new LoyaltyCardDisplayOption(
mContext.getString(R.string.show_validity),
showingValidity(),
this::showValidity
));
// Hide "Show archived cards" option unless the callback exists
if (mSwapCursorCallback != null) {
displayOptions.add(new LoyaltyCardDisplayOption(
mContext.getString(R.string.show_archived_cards),
showingArchivedCards(),
this::showArchivedCards
));
}
// We need to convert Boolean[] to boolean[]
boolean[] values = new boolean[displayOptions.size()];
for (int i = 0; i < values.length; i++) values[i] = displayOptions.get(i).value;
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(mContext);
builder.setTitle(R.string.action_display_options);
builder.setMultiChoiceItems(
displayOptions.stream().map(x -> x.name).toArray(String[]::new),
values,
(dialogInterface, i, b) -> displayOptions.get(i).callback.accept(b)
);
builder.setPositiveButton(R.string.ok, (dialog, i) -> dialog.dismiss());
AlertDialog dialog = builder.create();
dialog.show();
}
}

View File

@@ -3,7 +3,6 @@ package protect.card_locker;
import android.app.Application;
import androidx.appcompat.app.AppCompatDelegate;
import protect.card_locker.preferences.Settings;
public class LoyaltyCardLockerApplication extends Application {

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