Compare commits

..

1 Commits

Author SHA1 Message Date
Sylvia van Os
cd5ef267e8 WIP 2024-05-21 18:49:52 +02:00
1058 changed files with 3725 additions and 12177 deletions

View File

@@ -24,49 +24,31 @@ permissions:
security-events: none
statuses: none
env:
JAVA_HOME: /usr/lib/jvm/java-21-openjdk-amd64
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 21
run: |
sudo apt-get update
sudo apt-get install -y openjdk-21-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 35)
uses: ReactiveCircus/android-emulator-runner@v2
with:
api-level: 35
arch: x86_64
script: ./gradlew connected${{ matrix.flavor }}DebugAndroidTest
- name: Archive test results
if: always()
uses: actions/upload-artifact@v4.6.2
with:
name: test-results-flavor${{ matrix.flavor }}
path: app/build/reports
- uses: actions/checkout@v4.1.6
- 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@v3
- 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 assembleRelease
- name: Check lint
run: ./gradlew lintRelease
- name: Run unit tests
run: timeout 5m ./gradlew testReleaseUnitTest || { ./gradlew --stop && timeout 5m ./gradlew testReleaseUnitTest; }
- name: SpotBugs
run: ./gradlew spotbugsRelease
- name: Archive test results
if: always()
uses: actions/upload-artifact@v4.3.3
with:
name: test-results
path: app/build/reports

View File

@@ -27,15 +27,15 @@ jobs:
steps:
- name: Checkout repo
id: checkout
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.1.6
- name: Setup Python
uses: actions/setup-python@v5.6.0
uses: actions/setup-python@v5.1.0
with:
python-version: '3.x'
- name: Run converter script
run: python .scripts/changelog_to_fastlane.py
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7.0.8
uses: peter-evans/create-pull-request@v6.0.5
with:
title: "Update Fastlane changelogs"
commit-message: "Update Fastlane changelogs"

View File

@@ -25,7 +25,7 @@ jobs:
steps:
- name: Checkout repo
id: checkout
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.1.6
- name: Update contributors
id: update_contributors
uses: TheLastProject/contributors-to-file-action@v3.2.0
@@ -33,7 +33,7 @@ jobs:
file_in_repo: app/src/main/res/raw/contributors.txt
min_commit_count: 5
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7.0.8
uses: peter-evans/create-pull-request@v6.0.5
with:
title: "Update contributors"
commit-message: "Update contributors"

View File

@@ -6,7 +6,6 @@ on:
- main
paths:
- 'fastlane/**/title.txt'
- '.scripts/generate_feature_graphic/**'
permissions:
actions: none
checks: none
@@ -25,11 +24,11 @@ jobs:
generate-feature-graphic:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.1.6
- name: Install requirements
run: |
sudo apt-get update
sudo apt-get install inkscape mat2 optipng xvfb
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
@@ -39,7 +38,7 @@ jobs:
- name: Generate featureGraphic.png for each language
run: .scripts/generate_feature_graphic/generate_feature_graphic.sh
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7.0.8
uses: peter-evans/create-pull-request@v6.0.5
with:
title: "Update feature graphic"
commit-message: "Update feature graphic"

33
.github/workflows/gradle-update.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
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.1.6
- uses: obfusk/gradle-update-action@v2.0.0
id: gradle-update
- uses: gradle/actions/wrapper-validation@v3
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6.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

@@ -25,13 +25,13 @@ jobs:
update-locales:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v4.1.6
- 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.8
uses: peter-evans/create-pull-request@v6.0.5
with:
title: "Update locales"
commit-message: "Update locales"

3
.gitignore vendored
View File

@@ -25,6 +25,3 @@
/.bundle/
/vendor/bundle
/lib/bundler/man/
# Catima-specific
SHA256SUMS

View File

@@ -1,6 +1,6 @@
<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 direction="ltr" fill="white" xml:space="preserve" style="" font-family="Yesteryear" font-size="150" letter-spacing="0em"><tspan x="470.082" y="285.511">Catima
<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"/>
@@ -11,5 +11,5 @@
<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 direction="ltr" 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>
<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

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -4,11 +4,6 @@ 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
@@ -37,20 +32,19 @@ for lang in "$script_location/../../fastlane/metadata/android/"*; do
# 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 ;;
fa-IR) sed -i -e 's/svg direction="ltr"/svg direction="rtl"/' -e "s/Yesteryear/Noto Sans Arabic/" -e "s/Lexend Deca/Noto Sans Arabic/" featureGraphic.svg ;;
hi-IN) sed -i -e "s/Yesteryear/Noto Sans Devanagari/" -e "s/Lexend Deca/Noto Serif Devanagari/" featureGraphic.svg ;;
hi-IN) sed -i -e "s/Yesteryear/Noto Serif 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 ;;
kn-IN) sed -i -e 's/font-size="150"/font-size="100"/' -e 's/y="285.511"/y="235.511"/' 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 ;;
zh-TW) sed -i "s/Lexend Deca/Noto Serif CJK TC/" featureGraphic.svg ;;
*) ;;
esac
fi
# Ensure images directory exists
mkdir -p images
# Generate .png (we use Inkscape because ImageMagick ignores RTL)
xvfb-run inkscape --export-filename=images/featureGraphic.png featureGraphic.svg
# Generate .png
convert featureGraphic.svg images/featureGraphic.png
# Optimize .png
optipng images/featureGraphic.png
# Remove metadata (timestamps) from .png

View File

@@ -24,7 +24,7 @@ sed = [
]
subprocess.run(sed, check=True)
with open("app/src/main/res/xml/locales_config.xml", "w", encoding="utf-8") as fh:
with open("app/src/main/res/xml/locales_config.xml", "w") 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')

View File

@@ -11,7 +11,6 @@ MIN_PERCENT = 90
NOT_LANGS = ("night", "w600dp")
REPLACE_CODES = {
"el": "el-rGR",
"he": "iw",
"id": "in-rID",
"ro": "ro-rRO",
"zh_Hans": "zh-rCN",
@@ -20,27 +19,15 @@ REPLACE_CODES = {
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
r = requests.get(STATS_URL, timeout=5)
r.raise_for_status()
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")
for lang in r.json()["results"]:
if lang["code"] != "en":
code = REPLACE_CODES.get(lang["code"], lang["code"]).replace("_", "-r")
results.append((code, round(lang["translated_percent"])))
return sorted(results)
def get_dir_langs() -> List[str]:
@@ -55,7 +42,7 @@ def get_dir_langs() -> List[str]:
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:
with open("app/src/main/res/values/settings.xml") as fh:
for line in fh:
if not in_section and 'name="locale_values"' in line:
in_section = True
@@ -72,7 +59,7 @@ def get_xml_langs() -> List[Tuple[str, bool]]:
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:
with open("app/src/main/res/values/settings.xml") as fh:
for line in fh:
if not in_section and 'name="locale_values"' in line:
in_section = True
@@ -83,7 +70,7 @@ def update_xml_langs(langs: List[Tuple[str, bool]]) -> None:
else:
continue
lines.append(line)
with open("app/src/main/res/values/settings.xml", "w", encoding="utf-8") as fh:
with open("app/src/main/res/values/settings.xml", "w") as fh:
for line in lines:
fh.write(line)

View File

@@ -1,86 +1,9 @@
# Changelog
## v2.36.0 - 150 (2025-08-05)
- Add a widget showing all non-archived cards
- Prevent the keyboard from overlapping the save button in edit and group screens
## v2.35.1 - 149 (2025-06-17)
- Dependency and translation updates
## v2.35.0 - 148 (2025-05-17)
- Add ability to choose barcode width in fullscreen view
- Remove confusing import from app function
- Various scanning fixes
- Fix crash when loading a pkpass file without barcode
## v2.34.5 - 147 (2025-03-22)
- Target Android 15
- Fix crash reading unsupported pkpass files
- Improve pkpass support
## v2.34.4 - 146 (2025-01-17)
- Ability to sort cards by start of validity
- Temporarily revert to targeting Android 14 to fix some UI issues
## v2.34.3 - 145 (2025-01-15)
- Target Android 15
- Fix keyboard covering save button in edit screen
- Fix some pkpass files not being detected as pkpass (application/vnd-com.apple.pkpass mime type support)
## 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)
## Unreleased - 136
- 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)

View File

@@ -1,128 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
catima.g9ex3@hackerchick.me.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -36,6 +36,10 @@ These are the Android lint checker, run using:
# ./gradlew lintRelease
and SpotBugs, run using:
# ./gradlew spotbugsRelease
The final check is by testing the application on a live device and verifying
the basic functionality works as expected.

View File

@@ -1,8 +1,3 @@
source "https://rubygems.org"
gem "fastlane"
# https://github.com/fastlane/fastlane/issues/29183
gem "abbrev"
gem "mutex_m"
gem "ostruct"

View File

@@ -5,44 +5,41 @@ GEM
base64
nkf
rexml
abbrev (0.1.2)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.4.0)
aws-partitions (1.1117.0)
aws-sdk-core (3.226.0)
aws-eventstream (1.3.0)
aws-partitions (1.931.0)
aws-sdk-core (3.196.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.105.0)
aws-sdk-core (~> 3, >= 3.225.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.189.1)
aws-sdk-core (~> 3, >= 3.225.0)
aws-sdk-kms (1.81.0)
aws-sdk-core (~> 3, >= 3.193.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.151.0)
aws-sdk-core (~> 3, >= 3.194.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.3.0)
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.7.0)
digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.112.0)
faraday (1.10.4)
excon (0.110.0)
faraday (1.10.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@@ -58,20 +55,20 @@ GEM
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.1)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.1.1)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-multipart (1.0.4)
multipart-post (~> 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.4.0)
fastlane (2.228.0)
fastimage (2.3.1)
fastlane (2.220.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -87,7 +84,6 @@ 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)
@@ -111,10 +107,8 @@ GEM
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.4.1)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
@@ -132,12 +126,12 @@ GEM
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.8.0)
google-cloud-core (1.7.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.5.0)
google-cloud-errors (1.4.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
@@ -153,40 +147,37 @@ GEM
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.9.0)
mutex_m
httpclient (2.8.3)
jmespath (1.6.2)
json (2.12.2)
jwt (2.10.1)
json (2.7.2)
jwt (2.8.1)
base64
logger (1.7.0)
mini_magick (4.13.2)
mini_magick (4.12.0)
mini_mime (1.1.5)
multi_json (1.15.0)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
naturally (2.3.0)
nanaimo (0.3.0)
naturally (2.2.1)
nkf (0.2.0)
optparse (0.6.0)
optparse (0.5.0)
os (1.1.4)
ostruct (0.6.1)
plist (3.7.2)
public_suffix (6.0.2)
rake (13.3.0)
plist (3.7.1)
public_suffix (5.0.5)
rake (13.2.1)
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.4.1)
rouge (3.28.0)
rexml (3.2.8)
strscan (>= 3.0.9)
rouge (2.0.7)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
rubyzip (2.3.2)
security (0.1.5)
signet (0.20.0)
signet (0.19.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
@@ -194,7 +185,7 @@ GEM
simctl (1.6.10)
CFPropertyList
naturally
sysrandom (1.0.5)
strscan (3.1.0)
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
@@ -204,17 +195,17 @@ GEM
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unicode-display_width (2.6.0)
unicode-display_width (2.5.0)
word_wrap (1.0.0)
xcodeproj (1.27.0)
xcodeproj (1.24.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.1)
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)
@@ -222,10 +213,7 @@ PLATFORMS
ruby
DEPENDENCIES
abbrev
fastlane
mutex_m
ostruct
BUNDLED WITH
2.5.22
2.5.9

View File

@@ -1,34 +1,33 @@
import com.android.build.gradle.internal.tasks.factory.dependsOn
import com.github.spotbugs.snom.SpotBugsTask
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.github.spotbugs")
}
kotlin {
jvmToolchain(21)
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 = 35
compileSdk = 34
defaultConfig {
applicationId = "me.hackerchick.catima"
minSdk = 21
targetSdk = 35
versionCode = 150
versionName = "2.36.0"
targetSdk = 34
versionCode = 135
versionName = "2.29.1"
vectorDrawables.useSupportLibrary = true
multiDexEnabled = true
resourceConfigurations += listOf("ar", "be", "bg", "bn", "bn-rIN", "bs", "cs", "da", "de", "el-rGR", "en", "eo", "es", "es-rAR", "et", "fa", "fi", "fr", "gl", "he-rIL", "hi", "hr", "hu", "in-rID", "is", "it", "ja", "ko", "lt", "lv", "nb-rNO", "nl", "oc", "pl", "pt", "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")
resourceConfigurations += listOf("ar", "bg", "bn", "bn-rIN", "bs", "cs", "da", "de", "el-rGR", "en", "eo", "es", "es-rAR", "fi", "fr", "he-rIL", "hi", "hr", "hu", "in-rID", "is", "it", "ja", "ko", "lt", "lv", "nb-rNO", "nl", "oc", "pl", "pt-rPT", "ro-rRO", "ru", "sk", "sl", "sv", "tr", "uk", "vi", "zh-rCN", "zh-rTW")
}
buildTypes {
@@ -49,21 +48,6 @@ android {
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
@@ -76,8 +60,8 @@ android {
// Flag to enable support for the new language APIs
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
sourceSets {
@@ -98,49 +82,50 @@ android {
lint {
lintConfig = file("lint.xml")
}
kotlinOptions {
jvmTarget = "21"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
}
dependencies {
// AndroidX
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
implementation("androidx.core:core-ktx:1.16.0")
implementation("androidx.core:core-remoteviews:1.1.0")
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("androidx.exifinterface:exifinterface:1.4.1")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
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.5")
implementation("com.github.yalantis:ucrop:2.2.9")
implementation("androidx.work:work-runtime:2.9.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
// Splash Screen
implementation("androidx.core:core-splashscreen:1.0.1")
// 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")
// Testing
val androidXTestVersion = "1.7.0"
val junitVersion = "4.13.2"
testImplementation("androidx.test:core:$androidXTestVersion")
testImplementation("junit:junit:$junitVersion")
testImplementation("org.robolectric:robolectric:4.15.1")
// SpotBugs
implementation("io.wcm.tooling.spotbugs:io.wcm.tooling.spotbugs.annotations:1.0.0")
androidTestImplementation("androidx.test:core:$androidXTestVersion")
androidTestImplementation("junit:junit:$junitVersion")
androidTestImplementation("androidx.test.ext:junit:1.3.0")
androidTestImplementation("androidx.test:runner:$androidXTestVersion")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
// Testing
testImplementation("androidx.test:core:1.5.0")
testImplementation("junit:junit:4.13.2")
testImplementation("org.robolectric:robolectric:4.12.2")
}
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) {

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 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima Fehlersuche</string>
</resources>
<resources></resources>

View File

@@ -1,4 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Отстраняване на грешки в Catima</string>
</resources>
<resources></resources>

View File

@@ -1,4 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">ক্যাটিমা ডিবাগ</string>
</resources>
<resources></resources>

View File

@@ -1,4 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">ক্যাটিমা ডিবাগ</string>
</resources>
<resources></resources>

View File

@@ -1,4 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima Otklanjanje Grešaka</string>
</resources>
<resources></resources>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima Depuración</string>
</resources>
<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">عیب‌یابی کاتیما</string>
</resources>

View File

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

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Débogage de Catima</string>
</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 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima Villuleit</string>
</resources>
<resources></resources>

View File

@@ -1,4 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catimaデバーグ</string>
</resources>
<resources></resources>

View File

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

View File

@@ -1,4 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima Klaidų Taisymas</string>
</resources>
<resources></resources>

View File

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

View File

@@ -1,4 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">കാറ്റിമ ഡീബഗ്</string>
</resources>
<resources></resources>

View File

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

View File

@@ -1,4 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima Correcion d\'Errors</string>
</resources>
<resources></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 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima Odpravljanje Napak</string>
</resources>
<resources></resources>

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Catima Hata Ayıklama</string>
</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">ⴽⴰⵜⵉⵎⴰ ⴰⵙⵔⴰⵡ</string>
</resources>

View File

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

View File

@@ -12,11 +12,13 @@
<uses-sdk tools:overrideLibrary="com.google.zxing.client.android" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="23" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
android:required="true" />
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false" />
@@ -30,20 +32,6 @@
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:localeConfig="@xml/locales_config">
<receiver
android:name=".ListWidget"
android:label="@string/card_list_widget_name"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/list_widget_info" />
</receiver>
<activity
android:name=".MainActivity"
android:exported="true"
@@ -53,29 +41,13 @@
<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" />
<data android:mimeType="application/vnd-com.apple.pkpass" />
<data android:mimeType="application/vnd.espass-espass" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
<data android:mimeType="image/*" />
<data android:mimeType="application/pdf" />
<data android:mimeType="application/vnd.apple.pkpass" />
<data android:mimeType="application/vnd-com.apple.pkpass" />
<data android:mimeType="application/vnd.espass-espass" />
</intent-filter>
</activity>
<activity
@@ -89,17 +61,17 @@
<activity
android:name=".ManageGroupActivity"
android:label="@string/group_edit"
android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="adjustResize"/>
android:theme="@style/AppTheme.NoActionBar"/>
<activity
android:name=".LoyaltyCardViewActivity"
android:exported="true"
android:theme="@style/AppTheme.NoActionBar" />
android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".LoyaltyCardEditActivity"
android:exported="true"
android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="adjustResize">
android:windowSoftInputMode="stateHidden">
<intent-filter
android:autoVerify="true"
android:label="@string/app_name">
@@ -137,7 +109,8 @@
<activity
android:name=".BarcodeSelectorActivity"
android:label="@string/selectBarcodeTitle"
android:theme="@style/AppTheme.NoActionBar" />
android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".preferences.SettingsActivity"
android:label="@string/settings"
@@ -212,11 +185,11 @@
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>
<service android:name=".importexport.ImportExportWorker"/>
</application>
</manifest>

View File

@@ -0,0 +1,147 @@
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 {
private static final String TAG = "Catima";
private AboutActivityBinding binding;
private AboutContent content;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = AboutActivityBinding.inflate(getLayoutInflater());
content = new AboutContent(this);
setTitle(content.getPageTitle());
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
enableToolbarBackButton();
TextView copyright = binding.creditsSub;
copyright.setText(content.getCopyrightShort());
TextView versionHistory = binding.versionHistorySub;
versionHistory.setText(content.getVersionHistory());
binding.versionHistory.setTag("https://catima.app/changelog/");
binding.translate.setTag("https://hosted.weblate.org/engage/catima/");
binding.license.setTag("https://github.com/CatimaLoyalty/Android/blob/main/LICENSE");
binding.repo.setTag("https://github.com/CatimaLoyalty/Android/");
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");
boolean installedFromGooglePlay = Utils.installedFromGooglePlay(this);
// Hide Google Play rate button if not on Google Play
binding.rate.setVisibility(installedFromGooglePlay ? View.VISIBLE : View.GONE);
// Hide donate button on Google Play (Google Play doesn't allow donation links)
binding.donate.setVisibility(installedFromGooglePlay ? View.GONE : View.VISIBLE);
bindClickListeners();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
finish();
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onDestroy() {
super.onDestroy();
content.destroy();
clearClickListeners();
binding = null;
}
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);
binding.credits.setOnClickListener(view -> showCredits());
}
private void clearClickListeners() {
binding.versionHistory.setOnClickListener(null);
binding.translate.setOnClickListener(null);
binding.license.setOnClickListener(null);
binding.repo.setOnClickListener(null);
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);
}
}
}

View File

@@ -1,149 +0,0 @@
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.StringRes
import androidx.core.view.isVisible
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import protect.card_locker.databinding.AboutActivityBinding
class AboutActivity : CatimaAppCompatActivity() {
private companion object {
private const val TAG = "Catima"
}
private lateinit var binding: AboutActivityBinding
private lateinit var content: AboutContent
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = AboutActivityBinding.inflate(layoutInflater)
content = AboutContent(this)
title = content.pageTitle
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
enableToolbarBackButton()
binding.apply {
creditsSub.text = content.copyrightShort
versionHistorySub.text = content.versionHistory
versionHistory.tag = "https://catima.app/changelog/"
translate.tag = "https://hosted.weblate.org/engage/catima/"
license.tag = "https://github.com/CatimaLoyalty/Android/blob/main/LICENSE"
repo.tag = "https://github.com/CatimaLoyalty/Android/"
privacy.tag = "https://catima.app/privacy-policy/"
reportError.tag = "https://github.com/CatimaLoyalty/Android/issues"
rate.tag = "https://play.google.com/store/apps/details?id=me.hackerchick.catima"
donate.tag = "https://catima.app/donate"
// Hide Google Play rate button if not on Google Play
rate.isVisible = BuildConfig.showRateOnGooglePlay
// Hide donate button on Google Play (Google Play doesn't allow donation links)
donate.isVisible = BuildConfig.showDonate
}
bindClickListeners()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
finish()
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onDestroy() {
super.onDestroy()
content.destroy()
clearClickListeners()
}
private fun bindClickListeners() {
binding.apply {
versionHistory.setOnClickListener { showHistory(it) }
translate.setOnClickListener { openExternalBrowser(it) }
license.setOnClickListener { showLicense(it) }
repo.setOnClickListener { openExternalBrowser(it) }
privacy.setOnClickListener { showPrivacy(it) }
reportError.setOnClickListener { openExternalBrowser(it) }
rate.setOnClickListener { openExternalBrowser(it) }
donate.setOnClickListener { openExternalBrowser(it) }
credits.setOnClickListener { showCredits() }
}
}
private fun clearClickListeners() {
binding.apply {
versionHistory.setOnClickListener(null)
translate.setOnClickListener(null)
license.setOnClickListener(null)
repo.setOnClickListener(null)
privacy.setOnClickListener(null)
reportError.setOnClickListener(null)
rate.setOnClickListener(null)
donate.setOnClickListener(null)
credits.setOnClickListener(null)
}
}
private fun showCredits() {
showHTML(R.string.credits, content.contributorInfo, null)
}
private fun showHistory(view: View) {
showHTML(R.string.version_history, content.historyInfo, view)
}
private fun showLicense(view: View) {
showHTML(R.string.license, content.licenseInfo, view)
}
private fun showPrivacy(view: View) {
showHTML(R.string.privacy_policy, content.privacyInfo, view)
}
private fun showHTML(@StringRes title: Int, text: Spanned, view: View?) {
val dialogContentPadding = resources.getDimensionPixelSize(R.dimen.alert_dialog_content_padding)
val textView = TextView(this).apply {
setText(text)
Utils.makeTextViewLinksClickable(this, text)
}
val scrollView = ScrollView(this).apply {
addView(textView)
setPadding(dialogContentPadding, dialogContentPadding / 2, dialogContentPadding, 0)
}
MaterialAlertDialogBuilder(this).apply {
setTitle(title)
setView(scrollView)
setPositiveButton(R.string.ok, null)
// Add View online button if an URL is linked to this view
view?.tag?.let {
setNeutralButton(R.string.view_online) { _, _ -> openExternalBrowser(view) }
}
show()
}
}
private fun openExternalBrowser(view: View) {
val tag = view.tag
if (tag is String && tag.startsWith("https://")) {
OpenWebLinkHandler().openBrowser(this, tag)
}
}
}

View File

@@ -49,7 +49,7 @@ public class BarcodeImageWriterTask implements CompatCallable<Bitmap> {
BarcodeImageWriterTask(
Context context, ImageView imageView, String cardIdString,
CatimaBarcode barcodeFormat, TextView textView,
boolean showFallback, BarcodeImageWriterResultCallback callback, boolean roundCornerPadding, boolean isFullscreen
boolean showFallback, BarcodeImageWriterResultCallback callback, boolean roundCornerPadding
) {
mContext = context;
@@ -86,13 +86,13 @@ public class BarcodeImageWriterTask implements CompatCallable<Bitmap> {
if (format.isSquare()) {
imageHeight = imageWidth = Math.min(imageViewHeight, Math.min(MAX_WIDTH, imageViewWidth));
} else if (imageView.getWidth() < MAX_WIDTH && !isFullscreen) {
} else if (imageView.getWidth() < MAX_WIDTH) {
imageHeight = imageViewHeight;
imageWidth = imageViewWidth;
} else {
// Scale down the image to reduce the memory needed to produce it
imageWidth = Math.min(MAX_WIDTH, this.mContext.getResources().getDisplayMetrics().widthPixels);
double ratio = (double) imageWidth / (double) imageViewWidth;
imageWidth = MAX_WIDTH;
double ratio = (double) MAX_WIDTH / (double) imageViewWidth;
imageHeight = (int) (imageViewHeight * ratio);
}

View File

@@ -0,0 +1,125 @@
package protect.card_locker;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
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 protect.card_locker.databinding.BarcodeSelectorActivityBinding;
/**
* This activity is callable and will allow a user to enter
* barcode data and generate all barcodes possible for
* the data. The user may then select any barcode, where its
* data and type will be returned to the caller.
*/
public class BarcodeSelectorActivity extends CatimaAppCompatActivity implements BarcodeSelectorAdapter.BarcodeSelectorListener {
private BarcodeSelectorActivityBinding binding;
private static final String TAG = "Catima";
// Result this activity will return
public static final String BARCODE_CONTENTS = "contents";
public static final String BARCODE_FORMAT = "format";
private final Handler typingDelayHandler = new Handler(Looper.getMainLooper());
public static final Integer INPUT_DELAY = 250;
private BarcodeSelectorAdapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = BarcodeSelectorActivityBinding.inflate(getLayoutInflater());
setTitle(R.string.selectBarcodeTitle);
setContentView(binding.getRoot());
Toolbar toolbar = binding.toolbar;
setSupportActionBar(toolbar);
enableToolbarBackButton();
EditText cardId = binding.cardId;
ListView mBarcodeList = binding.barcodes;
mAdapter = new BarcodeSelectorAdapter(this, new ArrayList<>(), this);
mBarcodeList.setAdapter(mAdapter);
cardId.addTextChangedListener(new SimpleTextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// Delay the input processing so we avoid overload
typingDelayHandler.removeCallbacksAndMessages(null);
typingDelayHandler.postDelayed(() -> {
Log.d(TAG, "Entered text: " + s);
runOnUiThread(() -> {
generateBarcodes(s.toString());
});
}, INPUT_DELAY);
}
});
final Bundle b = getIntent().getExtras();
final String initialCardId = b != null ? b.getString("initialCardId") : null;
if (initialCardId != null) {
cardId.setText(initialCardId);
} else {
generateBarcodes("");
}
}
private void generateBarcodes(String value) {
// Update barcodes
ArrayList<CatimaBarcodeWithValue> barcodes = new ArrayList<>();
for (BarcodeFormat barcodeFormat : CatimaBarcode.barcodeFormats) {
CatimaBarcode catimaBarcode = CatimaBarcode.fromBarcode(barcodeFormat);
barcodes.add(new CatimaBarcodeWithValue(catimaBarcode, value));
}
mAdapter.setBarcodes(barcodes);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
setResult(Activity.RESULT_CANCELED);
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onRowClicked(int inputPosition, View view) {
CatimaBarcodeWithValue barcodeWithValue = mAdapter.getItem(inputPosition);
CatimaBarcode catimaBarcode = barcodeWithValue.catimaBarcode();
if (!mAdapter.isValid(view)) {
Toast.makeText(this, getString(R.string.wrongValueForBarcodeType), Toast.LENGTH_LONG).show();
return;
}
String barcodeFormat = catimaBarcode.format().name();
String value = barcodeWithValue.value();
Log.d(TAG, "Selected barcode type " + barcodeFormat);
Intent result = new Intent();
result.putExtra(BARCODE_FORMAT, barcodeFormat);
result.putExtra(BARCODE_CONTENTS, value);
BarcodeSelectorActivity.this.setResult(RESULT_OK, result);
finish();
}
}

View File

@@ -1,118 +0,0 @@
package protect.card_locker
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.core.view.MenuProvider
import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import protect.card_locker.BarcodeSelectorAdapter.BarcodeSelectorListener
import protect.card_locker.databinding.BarcodeSelectorActivityBinding
/**
* This activity is callable and will allow a user to enter
* barcode data and generate all barcodes possible for
* the data. The user may then select any barcode, where its
* data and type will be returned to the caller.
*/
class BarcodeSelectorActivity : CatimaAppCompatActivity(), BarcodeSelectorListener, MenuProvider {
private lateinit var binding: BarcodeSelectorActivityBinding
private lateinit var mAdapter: BarcodeSelectorAdapter
companion object {
private const val TAG = "Catima"
// Result this activity will return
const val BARCODE_CONTENTS = "contents"
const val BARCODE_FORMAT = "format"
const val INPUT_DELAY = 250L
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
addMenuProvider(this)
binding = BarcodeSelectorActivityBinding.inflate(layoutInflater)
setTitle(R.string.selectBarcodeTitle)
setContentView(binding.getRoot())
Utils.applyWindowInsets(binding.getRoot())
setSupportActionBar(binding.toolbar)
enableToolbarBackButton()
var typingDelayJob: Job? = null
val cardId = binding.cardId
val mBarcodeList = binding.barcodes
mAdapter = BarcodeSelectorAdapter(this, ArrayList<CatimaBarcodeWithValue?>(), this)
mBarcodeList.adapter = mAdapter
cardId.doOnTextChanged { s, _, _, _ ->
typingDelayJob?.cancel()
typingDelayJob =
lifecycleScope.launch {
delay(INPUT_DELAY) // Delay the input processing so we avoid overload
Log.d(TAG, "Entered text: $s")
generateBarcodes(s.toString())
}
}
val initialCardId = intent.extras?.getString(LoyaltyCard.BUNDLE_LOYALTY_CARD_CARD_ID)
initialCardId?.let {
cardId.setText(initialCardId)
} ?: generateBarcodes("")
}
private fun generateBarcodes(value: String?) {
// Update barcodes
val barcodes = ArrayList<CatimaBarcodeWithValue?>()
CatimaBarcode.barcodeFormats.forEach {
val catimaBarcode = CatimaBarcode.fromBarcode(it)
barcodes.add(CatimaBarcodeWithValue(catimaBarcode, value))
}
mAdapter.setBarcodes(barcodes)
}
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
if (menuItem.itemId == android.R.id.home) {
setResult(RESULT_CANCELED)
finish()
}
return true
}
override fun onRowClicked(inputPosition: Int, view: View) {
val barcodeWithValue = mAdapter.getItem(inputPosition)
val catimaBarcode = barcodeWithValue!!.catimaBarcode()
if (!mAdapter.isValid(view)) {
Toast.makeText(this, getString(R.string.wrongValueForBarcodeType), Toast.LENGTH_LONG).show()
return
}
val barcodeFormat = catimaBarcode.format().name
val value = barcodeWithValue.value()
Log.d(TAG, "Selected barcode type $barcodeFormat")
Intent().apply {
putExtra(BARCODE_FORMAT, barcodeFormat)
putExtra(BARCODE_CONTENTS, value)
setResult(RESULT_OK, this)
}
finish()
}
}

View File

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

View File

@@ -0,0 +1,26 @@
package protect.card_locker;
public class BarcodeValues {
private final String mFormat;
private final String mContent;
private String mNote;
public BarcodeValues(String format, String content) {
mFormat = format;
mContent = content;
}
public void setNote(String note) {
mNote = note;
}
public String format() {
return mFormat;
}
public String content() {
return mContent;
}
public String note() { return mNote; }
}

View File

@@ -0,0 +1,6 @@
package protect.card_locker;
public interface BarcodeValuesListDisambiguatorCallback {
void onUserChoseBarcode(BarcodeValues barcodeValues);
void onUserDismissedSelector();
}

View File

@@ -0,0 +1,105 @@
package protect.card_locker;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
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.SimpleToolbarListActivityBinding;
/**
* The configuration screen for creating a shortcut.
*/
public class CardShortcutConfigure extends CatimaAppCompatActivity implements LoyaltyCardCursorAdapter.CardAdapterListener {
private SimpleToolbarListActivityBinding binding;
static final String TAG = "Catima";
private SQLiteDatabase mDatabase;
private LoyaltyCardCursorAdapter mAdapter;
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
binding = SimpleToolbarListActivityBinding.inflate(getLayoutInflater());
mDatabase = new DBHelper(this).getReadableDatabase();
// Set the result to CANCELED. This will cause nothing to happen if the
// aback button is pressed.
setResult(RESULT_CANCELED);
setContentView(binding.getRoot());
Toolbar toolbar = binding.toolbar;
toolbar.setTitle(R.string.shortcutSelectCard);
setSupportActionBar(toolbar);
// If there are no cards, bail
int cardCount = DBHelper.getLoyaltyCardCount(mDatabase);
if (cardCount == 0) {
Toast.makeText(this, R.string.noCardsMessage, 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.All);
mAdapter = new LoyaltyCardCursorAdapter(this, cardCursor, this, null);
cardList.setAdapter(mAdapter);
}
private void onClickAction(int position) {
Cursor selected = DBHelper.getLoyaltyCardCursor(mDatabase, DBHelper.LoyaltyCardArchiveFilter.All);
selected.moveToPosition(position);
LoyaltyCard loyaltyCard = LoyaltyCard.toLoyaltyCard(selected);
Log.d(TAG, "Creating shortcut for card " + loyaltyCard.store + "," + loyaltyCard.id);
ShortcutInfoCompat shortcut = ShortcutHelper.createShortcutBuilder(CardShortcutConfigure.this, loyaltyCard).build();
setResult(RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(CardShortcutConfigure.this, shortcut));
finish();
}
@Override
public boolean onCreateOptionsMenu(Menu inputMenu) {
getMenuInflater().inflate(R.menu.card_details_menu, inputMenu);
return super.onCreateOptionsMenu(inputMenu);
}
@Override
public boolean onOptionsItemSelected(MenuItem inputItem) {
int id = inputItem.getItemId();
if (id == R.id.action_display_options) {
mAdapter.showDisplayOptionsDialog();
invalidateOptionsMenu();
return true;
}
return super.onOptionsItemSelected(inputItem);
}
@Override
public void onRowClicked(int inputPosition) {
onClickAction(inputPosition);
}
@Override
public void onRowLongClicked(int inputPosition) {
// do nothing
}
}

View File

@@ -1,96 +0,0 @@
package protect.card_locker
import android.database.sqlite.SQLiteDatabase
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.widget.Toast
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.view.MenuProvider
import androidx.recyclerview.widget.GridLayoutManager
import protect.card_locker.LoyaltyCardCursorAdapter.CardAdapterListener
import protect.card_locker.databinding.CardShortcutConfigureActivityBinding
import protect.card_locker.preferences.Settings
class CardShortcutConfigure : CatimaAppCompatActivity(), CardAdapterListener, MenuProvider {
private lateinit var binding: CardShortcutConfigureActivityBinding
private lateinit var mDatabase: SQLiteDatabase
private lateinit var mAdapter: LoyaltyCardCursorAdapter
private companion object {
private const val TAG: String = "Catima"
}
public override fun onCreate(savedInstanceBundle: Bundle?) {
super.onCreate(savedInstanceBundle)
addMenuProvider(this)
binding = CardShortcutConfigureActivityBinding.inflate(layoutInflater)
mDatabase = DBHelper(this).readableDatabase
// Set the result to CANCELED.
// This will cause nothing to happen if the back button is pressed.
setResult(RESULT_CANCELED)
setContentView(binding.getRoot())
Utils.applyWindowInsets(binding.getRoot())
binding.toolbar.apply {
setTitle(R.string.shortcutSelectCard)
setSupportActionBar(this)
}
// If there are no cards, bail
if (DBHelper.getLoyaltyCardCount(mDatabase) == 0) {
Toast.makeText(this, R.string.noCardsMessage, Toast.LENGTH_LONG).show()
finish()
}
val cardCursor = DBHelper.getLoyaltyCardCursor(mDatabase, DBHelper.LoyaltyCardArchiveFilter.All)
mAdapter = LoyaltyCardCursorAdapter(this, cardCursor, this, null)
binding.list.setAdapter(mAdapter)
}
override fun onResume() {
super.onResume()
val layoutManager = binding.list.layoutManager as GridLayoutManager?
layoutManager?.setSpanCount(Settings(this).getPreferredColumnCount())
}
private fun onClickAction(position: Int) {
val selected = DBHelper.getLoyaltyCardCursor(mDatabase, DBHelper.LoyaltyCardArchiveFilter.All)
selected.moveToPosition(position)
val loyaltyCard = LoyaltyCard.fromCursor(this, selected)
Log.d(TAG, "Creating shortcut for card ${loyaltyCard.store}, ${loyaltyCard.id}")
val shortcut = ShortcutHelper.createShortcutBuilder(this, loyaltyCard).build()
setResult(RESULT_OK,
ShortcutManagerCompat.createShortcutResultIntent(this, shortcut))
finish()
}
override fun onCreateMenu(inputMenu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.card_details_menu, inputMenu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
if (menuItem.itemId == R.id.action_display_options) {
mAdapter.showDisplayOptionsDialog()
}
return true
}
override fun onRowClicked(inputPosition: Int) {
onClickAction(inputPosition)
}
override fun onRowLongClicked(inputPosition: Int) {
// do nothing
}
}

View File

@@ -42,10 +42,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)
@@ -69,11 +69,11 @@ public class CardsOnPowerScreenService extends ControlsProviderService {
for (String controlId : controlIds) {
Control control;
Integer cardId = this.controlIdToCardId(controlId);
LoyaltyCard card = DBHelper.getLoyaltyCard(this, mDatabase, cardId);
LoyaltyCard card = DBHelper.getLoyaltyCard(mDatabase, cardId);
if (card != null) {
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)
@@ -99,7 +99,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 +129,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

@@ -7,7 +7,6 @@ import android.os.Bundle;
import android.view.View;
import android.view.Window;
import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
@@ -25,7 +24,6 @@ public class CatimaAppCompatActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
EdgeToEdge.enable(this);
super.onCreate(savedInstanceState);
Utils.patchColors(this);
}

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) {

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

@@ -23,11 +23,10 @@ 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 = 17;
public static final int DATABASE_VERSION = 16;
// NB: changing these values requires a migration
// NB: changing this value requires a migration
public static final int DEFAULT_ZOOM_LEVEL = 100;
public static final int DEFAULT_ZOOM_LEVEL_WIDTH = 100;
public static class LoyaltyCardDbGroups {
public static final String TABLE = "groups";
@@ -52,7 +51,6 @@ public class DBHelper extends SQLiteOpenHelper {
public static final String STAR_STATUS = "starstatus";
public static final String LAST_USED = "lastused";
public static final String ZOOM_LEVEL = "zoomlevel";
public static final String ZOOM_LEVEL_WIDTH = "zoomlevelwidth";
public static final String ARCHIVE_STATUS = "archive";
}
@@ -72,7 +70,6 @@ public class DBHelper extends SQLiteOpenHelper {
public enum LoyaltyCardOrder {
Alpha,
LastUsed,
ValidFrom,
Expiry
}
@@ -115,7 +112,6 @@ public class DBHelper extends SQLiteOpenHelper {
LoyaltyCardDbIds.STAR_STATUS + " INTEGER DEFAULT '0'," +
LoyaltyCardDbIds.LAST_USED + " INTEGER DEFAULT '0', " +
LoyaltyCardDbIds.ZOOM_LEVEL + " INTEGER DEFAULT '" + DEFAULT_ZOOM_LEVEL + "', " +
LoyaltyCardDbIds.ZOOM_LEVEL_WIDTH + " INTEGER DEFAULT '" + DEFAULT_ZOOM_LEVEL_WIDTH + "', " +
LoyaltyCardDbIds.ARCHIVE_STATUS + " INTEGER DEFAULT '0' )");
// create associative table for cards in groups
@@ -330,21 +326,16 @@ public class DBHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE
+ " ADD COLUMN " + LoyaltyCardDbIds.VALID_FROM + " INTEGER");
}
if (oldVersion < 17 && newVersion >= 17) {
db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE
+ " ADD COLUMN " + LoyaltyCardDbIds.ZOOM_LEVEL_WIDTH + " INTEGER DEFAULT '100' ");
}
}
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);
LoyaltyCard card = LoyaltyCard.toLoyaltyCard(cardCursor);
for (ImageLocationType imageLocationType : ImageLocationType.values()) {
String name = Utils.getCardImageFileName(card.id, imageLocationType);
if (card.getImageForImageLocationType(context, imageLocationType) != null) {
if (Utils.retrieveCardImageAsFile(context, name).exists()) {
files.add(name);
}
}
@@ -524,17 +515,15 @@ public class DBHelper extends SQLiteOpenHelper {
return (rowsUpdated == 1);
}
public static boolean updateLoyaltyCardZoomLevel(SQLiteDatabase database, int loyaltyCardId, int zoomLevel, int zoomLevelWidth) {
public static boolean updateLoyaltyCardZoomLevel(SQLiteDatabase database, int loyaltyCardId, int zoomLevel) {
ContentValues contentValues = new ContentValues();
contentValues.put(LoyaltyCardDbIds.ZOOM_LEVEL, zoomLevel);
contentValues.put(LoyaltyCardDbIds.ZOOM_LEVEL_WIDTH, zoomLevelWidth);
Log.d("updateLoyaltyCardZLevel", "Card Id = " + loyaltyCardId + " Zoom level= " + zoomLevel);
Log.d("updateLoyaltyCardZoomLW", "Card Id = " + loyaltyCardId + " Zoom level width= " + zoomLevelWidth);
int rowsUpdated = database.update(LoyaltyCardDbIds.TABLE, contentValues,
whereAttrs(LoyaltyCardDbIds.ID),
withArgs(loyaltyCardId));
Log.d("updateLoyaltyCardZLevel", "Card Id = " + loyaltyCardId + " Zoom level = " + zoomLevel + " Zoom level width = " + zoomLevelWidth);
return (rowsUpdated >= 1);
Log.d("updateLoyaltyCardZLevel", "Rows changed = " + rowsUpdated);
return (rowsUpdated == 1);
}
public static boolean updateLoyaltyCardBalance(SQLiteDatabase database, final int id, final BigDecimal newBalance) {
@@ -546,14 +535,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();
@@ -927,10 +916,6 @@ public class DBHelper extends SQLiteOpenHelper {
return LoyaltyCardDbIds.LAST_USED;
}
if (order == LoyaltyCardOrder.ValidFrom) {
return LoyaltyCardDbIds.VALID_FROM;
}
if (order == LoyaltyCardOrder.Expiry) {
return LoyaltyCardDbIds.EXPIRY;
}

View File

@@ -1,15 +1,15 @@
package protect.card_locker;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Context;
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;
@@ -18,31 +18,31 @@ import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.work.Data;
import androidx.work.ExistingWorkPolicy;
import androidx.work.OneTimeWorkRequest;
import androidx.work.OutOfQuotaPolicy;
import androidx.work.WorkManager;
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.Arrays;
import java.util.List;
import protect.card_locker.async.TaskHandler;
import protect.card_locker.databinding.ImportExportActivityBinding;
import protect.card_locker.importexport.DataFormat;
import protect.card_locker.importexport.ImportExportResult;
import protect.card_locker.importexport.ImportExportResultType;
import protect.card_locker.importexport.ImportExportWorker;
public class ImportExportActivity extends CatimaAppCompatActivity {
private ImportExportActivityBinding binding;
private static final String TAG = "Catima";
private ImportExportTask importExporter;
private String importAlertTitle;
private String importAlertMessage;
private DataFormat importDataFormat;
@@ -50,8 +50,12 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
private ActivityResultLauncher<Intent> fileCreateLauncher;
private ActivityResultLauncher<String> fileOpenLauncher;
private ActivityResultLauncher<Intent> filePickerLauncher;
final private TaskHandler mTasks = new TaskHandler();
private static final int PERMISSION_REQUEST_EXPORT = 100;
private static final int PERMISSION_REQUEST_IMPORT = 101;
private OneTimeWorkRequest mRequestedWorkRequest;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -59,14 +63,13 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
binding = ImportExportActivityBinding.inflate(getLayoutInflater());
setTitle(R.string.importExport);
setContentView(binding.getRoot());
Utils.applyWindowInsets(binding.getRoot());
Toolbar toolbar = binding.toolbar;
setSupportActionBar(toolbar);
enableToolbarBackButton();
Intent fileIntent = getIntent();
if (fileIntent != null && fileIntent.getType() != null) {
chooseImportType(fileIntent.getData());
chooseImportType(false, fileIntent.getData());
}
// would use ActivityResultContracts.CreateDocument() but mime type cannot be set
@@ -81,21 +84,20 @@ 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();
Data exportRequestData = new Data.Builder()
.putString(ImportExportWorker.INPUT_URI, uri.toString())
.putString(ImportExportWorker.INPUT_ACTION, ImportExportWorker.ACTION_EXPORT)
.putString(ImportExportWorker.INPUT_FORMAT, DataFormat.Catima.name())
.putString(ImportExportWorker.INPUT_PASSWORD, exportPassword)
.build();
mRequestedWorkRequest = new OneTimeWorkRequest.Builder(ImportExportWorker.class)
.setInputData(exportRequestData)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build();
PermissionUtils.requestPostNotificationsPermission(this, PERMISSION_REQUEST_EXPORT);
});
fileOpenLauncher = registerForActivityResult(new ActivityResultContracts.GetContent(), result -> {
if (result == null) {
@@ -104,6 +106,19 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
}
openFileForImport(result, null);
});
filePickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
Intent intent = result.getData();
if (intent == null) {
Log.e(TAG, "Activity returned NULL data");
return;
}
Uri uri = intent.getData();
if (uri == null) {
Log.e(TAG, "Activity returned NULL uri");
return;
}
openFileForImport(intent.getData(), null);
});
// Check that there is a file manager available
final Intent intentCreateDocumentAction = new Intent(Intent.ACTION_CREATE_DOCUMENT);
@@ -146,32 +161,35 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
// Check that there is a file manager available
Button importFilesystem = binding.importOptionFilesystemButton;
importFilesystem.setOnClickListener(v -> chooseImportType(null));
importFilesystem.setOnClickListener(v -> chooseImportType(false, 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);
// Check that there is an app that data can be imported from
Button importApplication = binding.importOptionApplicationButton;
importApplication.setOnClickListener(v -> chooseImportType(true, null));
}
public static OneTimeWorkRequest buildImportRequest(DataFormat dataFormat, Uri uri, char[] password) {
Data importRequestData = new Data.Builder()
.putString(ImportExportWorker.INPUT_URI, uri.toString())
.putString(ImportExportWorker.INPUT_ACTION, ImportExportWorker.ACTION_IMPORT)
.putString(ImportExportWorker.INPUT_FORMAT, dataFormat.name())
.putString(ImportExportWorker.INPUT_PASSWORD, Arrays.toString(password))
.build();
return new OneTimeWorkRequest.Builder(ImportExportWorker.class)
.setInputData(importRequestData)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build();
}
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();
mRequestedWorkRequest = buildImportRequest(importDataFormat, uri, password);
PermissionUtils.requestPostNotificationsPermission(this, PERMISSION_REQUEST_IMPORT);
}
private void chooseImportType(@Nullable Uri fileData) {
private void chooseImportType(boolean choosePicker,
@Nullable Uri fileData) {
List<CharSequence> betaImportOptions = new ArrayList<>();
betaImportOptions.add("Fidme");
@@ -234,7 +252,12 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
.setMessage(importAlertMessage)
.setPositiveButton(R.string.ok, (dialog1, which1) -> {
try {
fileOpenLauncher.launch("*/*");
if (choosePicker) {
final Intent intentPickAction = new Intent(Intent.ACTION_PICK);
filePickerLauncher.launch(intentPickAction);
} else {
fileOpenLauncher.launch("*/*");
}
} catch (ActivityNotFoundException e) {
Toast.makeText(getApplicationContext(), R.string.failedOpeningFileManager, Toast.LENGTH_LONG).show();
Log.e(TAG, "No activity found to handle intent", e);
@@ -246,60 +269,12 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
builder.show();
}
private void startImport(final InputStream target, final Uri targetUri, final DataFormat dataFormat, final char[] password, final boolean closeWhenDone) {
mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, true, false, false);
ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() {
@Override
public void onTaskComplete(ImportExportResult result, DataFormat dataFormat) {
onImportComplete(result, targetUri, dataFormat);
if (closeWhenDone) {
try {
target.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
};
importExporter = new ImportExportTask(ImportExportActivity.this,
dataFormat, target, password, listener);
mTasks.executeTask(TaskHandler.TYPE.IMPORT, importExporter);
}
private void startExport(final OutputStream target, final Uri targetUri, char[] password, final boolean closeWhenDone) {
mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, true, false, false);
ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() {
@Override
public void onTaskComplete(ImportExportResult result, DataFormat dataFormat) {
onExportComplete(result, targetUri);
if (closeWhenDone) {
try {
target.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
};
importExporter = new ImportExportTask(ImportExportActivity.this,
DataFormat.Catima, target, password, listener);
mTasks.executeTask(TaskHandler.TYPE.EXPORT, importExporter);
}
@Override
protected void onDestroy() {
mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, true, false, false);
mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, true, false, false);
super.onDestroy();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
setResult(RESULT_CANCELED);
finish();
return true;
}
@@ -307,19 +282,19 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
return super.onOptionsItemSelected(item);
}
private void retryWithPassword(DataFormat dataFormat, Uri uri) {
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
public static void retryWithPassword(Context context, DataFormat dataFormat, Uri uri) {
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context);
builder.setTitle(R.string.passwordRequired);
FrameLayout container = new FrameLayout(ImportExportActivity.this);
FrameLayout container = new FrameLayout(context);
final TextInputLayout textInputLayout = new TextInputLayout(ImportExportActivity.this);
final TextInputLayout textInputLayout = new TextInputLayout(context);
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(context);
input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
input.setHint(R.string.exportPasswordHint);
@@ -328,75 +303,55 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
builder.setView(container);
builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
openFileForImport(uri, input.getText().toString().toCharArray());
OneTimeWorkRequest importRequest = ImportExportActivity.buildImportRequest(dataFormat, uri, input.getText().toString().toCharArray());
WorkManager.getInstance(context).enqueueUniqueWork(ImportExportWorker.ACTION_IMPORT, ExistingWorkPolicy.REPLACE, importRequest);
});
builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> dialogInterface.cancel());
builder.show();
}
private String buildResultDialogMessage(ImportExportResult result, boolean isImport) {
int messageId;
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (result.resultType() == ImportExportResultType.Success) {
messageId = isImport ? R.string.importSuccessful : R.string.exportSuccessful;
} else {
messageId = isImport ? R.string.importFailed : R.string.exportFailed;
}
StringBuilder messageBuilder = new StringBuilder(getResources().getString(messageId));
if (result.developerDetails() != null) {
messageBuilder.append("\n\n");
messageBuilder.append(getResources().getString(R.string.include_if_asking_support));
messageBuilder.append("\n\n");
messageBuilder.append(result.developerDetails());
}
return messageBuilder.toString();
onMockedRequestPermissionsResult(requestCode, permissions, grantResults);
}
private void onImportComplete(ImportExportResult result, Uri path, DataFormat dataFormat) {
ImportExportResultType resultType = result.resultType();
public void onMockedRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
Integer failureReason = null;
if (resultType == ImportExportResultType.BadPassword) {
retryWithPassword(dataFormat, path);
return;
if (requestCode == PERMISSION_REQUEST_EXPORT) {
if (granted) {
WorkManager.getInstance(this).enqueueUniqueWork(ImportExportWorker.ACTION_EXPORT, ExistingWorkPolicy.REPLACE, mRequestedWorkRequest);
Toast.makeText(this, R.string.exportStartedCheckNotifications, Toast.LENGTH_LONG).show();
// Import/export started
setResult(RESULT_OK);
finish();
return;
}
failureReason = R.string.postNotificationsPermissionRequired;
} else if (requestCode == PERMISSION_REQUEST_IMPORT) {
if (granted) {
WorkManager.getInstance(this).enqueueUniqueWork(ImportExportWorker.ACTION_IMPORT, ExistingWorkPolicy.REPLACE, mRequestedWorkRequest);
// Import/export started
setResult(RESULT_OK);
finish();
return;
}
failureReason = R.string.postNotificationsPermissionRequired;
}
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(resultType == ImportExportResultType.Success ? R.string.importSuccessfulTitle : R.string.importFailedTitle);
builder.setMessage(buildResultDialogMessage(result, true));
builder.setNeutralButton(R.string.ok, (dialog, which) -> dialog.dismiss());
builder.create().show();
}
private void onExportComplete(ImportExportResult result, final Uri path) {
ImportExportResultType resultType = result.resultType();
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(resultType == ImportExportResultType.Success ? R.string.exportSuccessfulTitle : R.string.exportFailedTitle);
builder.setMessage(buildResultDialogMessage(result, false));
builder.setNeutralButton(R.string.ok, (dialog, which) -> dialog.dismiss());
if (resultType == ImportExportResultType.Success) {
final CharSequence sendLabel = ImportExportActivity.this.getResources().getText(R.string.sendLabel);
builder.setPositiveButton(sendLabel, (dialog, which) -> {
Intent sendIntent = new Intent(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_STREAM, path);
sendIntent.setType("text/csv");
// set flag to give temporary permission to external app to use the FileProvider
sendIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
ImportExportActivity.this.startActivity(Intent.createChooser(sendIntent,
sendLabel));
dialog.dismiss();
});
if (failureReason != null) {
Toast.makeText(this, failureReason, Toast.LENGTH_LONG).show();
}
builder.create().show();
}
}

View File

@@ -1,144 +0,0 @@
package protect.card_locker;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import android.widget.Toast;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import protect.card_locker.async.CompatCallable;
import protect.card_locker.importexport.DataFormat;
import protect.card_locker.importexport.ImportExportResult;
import protect.card_locker.importexport.ImportExportResultType;
import protect.card_locker.importexport.MultiFormatExporter;
import protect.card_locker.importexport.MultiFormatImporter;
public class ImportExportTask implements CompatCallable<ImportExportResult> {
private static final String TAG = "Catima";
private Activity activity;
private boolean doImport;
private DataFormat format;
private OutputStream outputStream;
private InputStream inputStream;
private char[] password;
private TaskCompleteListener listener;
private ProgressDialog progress;
/**
* Constructor which will setup a task for exporting to the given file
*/
ImportExportTask(Activity activity, DataFormat format, OutputStream output, char[] password,
TaskCompleteListener listener) {
super();
this.activity = activity;
this.doImport = false;
this.format = format;
this.outputStream = output;
this.password = password;
this.listener = listener;
}
/**
* Constructor which will setup a task for importing from the given InputStream.
*/
ImportExportTask(Activity activity, DataFormat format, InputStream input, char[] password,
TaskCompleteListener listener) {
super();
this.activity = activity;
this.doImport = true;
this.format = format;
this.inputStream = input;
this.password = password;
this.listener = listener;
}
private ImportExportResult performImport(Context context, InputStream stream, SQLiteDatabase database, char[] password) {
ImportExportResult importResult = MultiFormatImporter.importData(context, database, stream, format, password);
Log.i(TAG, "Import result: " + importResult);
return importResult;
}
private ImportExportResult performExport(Context context, OutputStream stream, SQLiteDatabase database, char[] password) {
ImportExportResult result;
try {
OutputStreamWriter writer = new OutputStreamWriter(stream, StandardCharsets.UTF_8);
result = MultiFormatExporter.exportData(context, database, stream, format, password);
writer.close();
} catch (IOException e) {
result = new ImportExportResult(ImportExportResultType.GenericFailure, e.toString());
Log.e(TAG, "Unable to export file", e);
}
Log.i(TAG, "Export result: " + result);
return result;
}
public void onPreExecute() {
progress = new ProgressDialog(activity);
progress.setTitle(doImport ? R.string.importing : R.string.exporting);
progress.setOnCancelListener(dialog -> cancel());
progress.setOnDismissListener(dialog -> cancel());
progress.show();
}
private void cancel() {
ImportExportTask.this.stop();
}
protected ImportExportResult doInBackground(Void... nothing) {
final SQLiteDatabase database = new DBHelper(activity).getWritableDatabase();
ImportExportResult result;
if (doImport) {
result = performImport(activity.getApplicationContext(), inputStream, database, password);
} else {
result = performExport(activity.getApplicationContext(), outputStream, database, password);
}
database.close();
return result;
}
public void onPostExecute(Object castResult) {
listener.onTaskComplete((ImportExportResult) castResult, format);
progress.dismiss();
Log.i(TAG, (doImport ? "Import" : "Export") + " Complete");
}
protected void onCancelled() {
progress.dismiss();
Log.i(TAG, (doImport ? "Import" : "Export") + " Cancelled");
}
protected void stop() {
// Whelp
}
@Override
public ImportExportResult call() {
return doInBackground();
}
interface TaskCompleteListener {
void onTaskComplete(ImportExportResult result, DataFormat format);
}
}

View File

@@ -125,30 +125,7 @@ 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,
100,
0,
null,
null,
null,
null,
null,
null
);
return new LoyaltyCard(-1, store, note, validFrom, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headerColor, 0, Utils.getUnixTime(), 100, 0);
} catch (NumberFormatException | UnsupportedEncodingException | ArrayIndexOutOfBoundsException ex) {
throw new InvalidObjectException("Not a valid import URI");
}

View File

@@ -1,130 +0,0 @@
package protect.card_locker
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.Icon
import android.os.Build
import android.view.View
import android.widget.RemoteViews
import androidx.core.widget.RemoteViewsCompat
import protect.card_locker.DBHelper.LoyaltyCardArchiveFilter
class ListWidget : AppWidgetProvider() {
fun updateAll(context: Context) {
val appWidgetManager = AppWidgetManager.getInstance(context)
val componentName = ComponentName(context, ListWidget::class.java)
onUpdate(
context,
appWidgetManager,
appWidgetManager.getAppWidgetIds(componentName)
)
}
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
for (appWidgetId in appWidgetIds) {
val database = DBHelper(context).readableDatabase
// Get cards
val order = Utils.getLoyaltyCardOrder(context);
val orderDirection = Utils.getLoyaltyCardOrderDirection(context);
val loyaltyCardCursor = DBHelper.getLoyaltyCardCursor(
database,
"",
null,
order,
orderDirection,
LoyaltyCardArchiveFilter.Unarchived
)
// Bind every card to cell in the grid
var hasCards = false
val remoteCollectionItemsBuilder = RemoteViewsCompat.RemoteCollectionItems.Builder()
if (loyaltyCardCursor.moveToFirst()) {
do {
val loyaltyCard = LoyaltyCard.fromCursor(context, loyaltyCardCursor)
remoteCollectionItemsBuilder.addItem(
loyaltyCard.id.toLong(),
createRemoteViews(
context, loyaltyCard
)
)
hasCards = true
} while (loyaltyCardCursor.moveToNext())
}
loyaltyCardCursor.close()
// Create the base empty view
var views = RemoteViews(context.packageName, R.layout.list_widget_empty)
if (hasCards) {
// If we have cards, create the list
views = RemoteViews(context.packageName, R.layout.list_widget)
val templateIntent = Intent(context, LoyaltyCardViewActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
context,
0,
templateIntent,
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
views.setPendingIntentTemplate(R.id.grid_view, pendingIntent)
RemoteViewsCompat.setRemoteAdapter(
context,
views,
appWidgetId,
R.id.grid_view,
remoteCollectionItemsBuilder.build()
)
}
// Let Android know the widget is ready for display
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
private fun createRemoteViews(context: Context, loyaltyCard: LoyaltyCard): RemoteViews {
// Create a single cell for the grid view, bind it to open in the LoyaltyCardViewActivity
// Note: Android 5 will not use bitmaps
val remoteViews = RemoteViews(context.packageName, R.layout.list_widget_item).apply {
val headerColor = Utils.getHeaderColor(context, loyaltyCard)
val foreground = if (Utils.needsDarkForeground(headerColor)) Color.BLACK else Color.WHITE
setInt(R.id.item_container_foreground, "setBackgroundColor", headerColor)
val icon = loyaltyCard.getImageThumbnail(context)
// setImageViewIcon is not supported on Android 5, so force Android 5 down the text path
if (icon != null && Build.VERSION.SDK_INT >= 23) {
setInt(R.id.item_container_foreground, "setBackgroundColor", foreground)
setImageViewIcon(R.id.item_image, Icon.createWithBitmap(icon))
setViewVisibility(R.id.item_text, View.INVISIBLE)
setViewVisibility(R.id.item_image, View.VISIBLE)
} else {
setImageViewBitmap(R.id.item_image, null)
setTextViewText(R.id.item_text, loyaltyCard.store)
setViewVisibility(R.id.item_text, View.VISIBLE)
setViewVisibility(R.id.item_image, View.INVISIBLE)
setTextColor(
R.id.item_text,
foreground
)
}
// Add the card ID to the intent template
val fillInIntent = Intent().apply {
putExtra(LoyaltyCardViewActivity.BUNDLE_ID, loyaltyCard.id)
}
setOnClickFillInIntent(R.id.item_container, fillInIntent)
}
return remoteViews
}
}

View File

@@ -1,9 +1,8 @@
package protect.card_locker;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -11,577 +10,142 @@ import androidx.annotation.Nullable;
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;
public class LoyaltyCard implements Parcelable {
public final int id;
public final String store;
public final String note;
@Nullable
public Date validFrom;
public final Date validFrom;
@Nullable
public Date expiry;
public BigDecimal balance;
public final Date expiry;
public final BigDecimal balance;
@Nullable
public Currency balanceType;
public String cardId;
public final Currency balanceType;
public final String cardId;
@Nullable
public String barcodeId;
public final String barcodeId;
@Nullable
public CatimaBarcode barcodeType;
public final 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 zoomLevelWidth;
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_ZOOM_LEVEL_WIDTH = "loyaltyCardZoomLevelWidth";
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);
setZoomLevelWidth(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 zoomLevelWidth
* @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,
@Nullable final Integer headerColor, final int starStatus,
final long lastUsed, final int zoomLevel, final int zoomLevelWidth, final int archiveStatus,
@Nullable Bitmap imageThumbnail, @Nullable String imageThumbnailPath,
@Nullable Bitmap imageFront, @Nullable String imageFrontPath,
@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);
setZoomLevelWidth(zoomLevelWidth);
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 setZoomLevelWidth(int zoomLevelWidth) {
if (zoomLevelWidth < 0 || zoomLevelWidth > 100) {
throw new IllegalArgumentException("zoomLevelWidth must be in range 0-100");
}
this.zoomLevelWidth = zoomLevelWidth;
}
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;
@Override
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 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_ZOOM_LEVEL_WIDTH)) {
setZoomLevelWidth(bundle.getInt(BUNDLE_LOYALTY_CARD_ZOOM_LEVEL_WIDTH));
} else if (requireFull) {
throw new IllegalArgumentException("Missing key " + BUNDLE_LOYALTY_CARD_ZOOM_LEVEL_WIDTH);
}
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_ZOOM_LEVEL_WIDTH)) {
bundle.putInt(BUNDLE_LOYALTY_CARD_ZOOM_LEVEL_WIDTH, zoomLevelWidth);
}
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
public static LoyaltyCard toLoyaltyCard(Cursor cursor) {
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
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));
// zoomLevel
int zoomLevel = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ZOOM_LEVEL));
// zoomLevelWidth
int zoomLevelWidth = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ZOOM_LEVEL_WIDTH));
// archiveStatus
int archiveStatus = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ARCHIVE_STATUS));
int archived = 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,
zoomLevelWidth,
archiveStatus,
null,
Utils.getCardImageFileName(id, ImageLocationType.icon),
null,
Utils.getCardImageFileName(id, ImageLocationType.front),
null,
Utils.getCardImageFileName(id, ImageLocationType.back)
);
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);
}
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*
public static boolean isDuplicate(final LoyaltyCard a, final LoyaltyCard b) {
// 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
@@ -595,23 +159,12 @@ public class LoyaltyCard {
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
a.archiveStatus == b.archiveStatus; // non-nullable int
}
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;
@Override
public int describeContents() {
return 0;
}
@NonNull
@@ -620,8 +173,7 @@ public class LoyaltyCard {
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 zoomLevelWidth=%s,%n archiveStatus=%s%n"
+ " imageThumbnail=%s,%n imageThumbnailPath=%s,%n imageFront=%s,%n imageFrontPath=%s,%n imageBack=%s,%n imageBackPath=%s,%n}",
+ " headerColor=%s,%n starStatus=%s,%n lastUsed=%s,%n zoomLevel=%s,%n archiveStatus=%s%n}",
this.id,
this.store,
this.note,
@@ -636,14 +188,19 @@ public class LoyaltyCard {
this.starStatus,
this.lastUsed,
this.zoomLevel,
this.zoomLevelWidth,
this.archiveStatus,
this.imageThumbnail,
this.imageThumbnailPath,
this.imageFront,
this.imageFrontPath,
this.imageBack,
this.imageBackPath
this.archiveStatus
);
}
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

@@ -30,7 +30,6 @@ import java.text.DateFormat;
import java.util.ArrayList;
import protect.card_locker.databinding.LoyaltyCardLayoutBinding;
import protect.card_locker.preferences.Settings;
public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCursorAdapter.LoyaltyCardListItemViewHolder> {
private int mCurrentSelectedIndex = -1;
@@ -80,7 +79,7 @@ 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) {
@@ -88,8 +87,8 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
boolean showDivider = false;
inputHolder.mDivider.setVisibility(View.GONE);
LoyaltyCard loyaltyCard = LoyaltyCard.fromCursor(mContext, inputCursor);
Bitmap icon = loyaltyCard.getImageThumbnail(mContext);
LoyaltyCard loyaltyCard = LoyaltyCard.toLoyaltyCard(inputCursor);
Bitmap icon = Utils.retrieveCardImage(mContext, loyaltyCard.id, ImageLocationType.icon);
if (mLoyaltyCardListDisplayOptions.showingNameBelowThumbnail() && icon != null) {
showDivider = true;
@@ -112,21 +111,21 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
}
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);
inputHolder.setExtraField(inputHolder.mValidFromField, DateFormat.getDateInstance(DateFormat.LONG).format(loyaltyCard.validFrom), Utils.isNotYetValid(loyaltyCard.validFrom) ? Color.RED : null, showDivider);
} else {
inputHolder.setExtraField(inputHolder.mValidFromField, null, null, false);
}
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);
inputHolder.setExtraField(inputHolder.mExpiryField, DateFormat.getDateInstance(DateFormat.LONG).format(loyaltyCard.expiry), Utils.hasExpired(loyaltyCard.expiry) ? Color.RED : null, showDivider);
} else {
inputHolder.setExtraField(inputHolder.mExpiryField, null, null, false);
}
inputHolder.mCardIcon.setContentDescription(loyaltyCard.store);
Utils.setIconOrTextWithBackground(mContext, loyaltyCard, icon, inputHolder.mCardIcon, inputHolder.mCardText, new Settings(mContext).getPreferredColumnCount());
inputHolder.mIconBackgroundColor = Utils.setIconOrTextWithBackground(mContext, loyaltyCard, icon, inputHolder.mCardIcon, inputHolder.mCardText);
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());
@@ -193,7 +192,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;
@@ -212,11 +211,13 @@ 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 ImageView mCardIcon, mStarBackground, mStarBorder, mTickIcon, mArchivedBackground;
public MaterialCardView mRow;
public ConstraintLayout mStar, mArchived;
public View mDivider;
private int mIconBackgroundColor;
protected LoyaltyCardListItemViewHolder(LoyaltyCardLayoutBinding loyaltyCardLayoutBinding, CardAdapterListener inputListener) {
super(loyaltyCardLayoutBinding.getRoot());
View inputView = loyaltyCardLayoutBinding.getRoot();
@@ -230,7 +231,10 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
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());
@@ -292,7 +296,31 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
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 +332,17 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
} else{
mArchived.setVisibility(View.GONE);
}
mStarBorder.invalidate();
mStarBackground.invalidate();
mArchivedBackground.invalidate();
}
}
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

@@ -19,7 +19,6 @@ import android.text.method.DigitsKeyListener;
import android.text.style.ForegroundColorSpan;
import android.text.util.Linkify;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@@ -52,7 +51,6 @@ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import com.google.android.material.color.MaterialColors;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.textfield.TextInputEditText;
import com.google.zxing.BarcodeFormat;
import java.io.File;
import java.io.UnsupportedEncodingException;
@@ -99,35 +97,9 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
static final String STATE_IMAGEINDEX = "imageIndex";
static final String STATE_FULLSCREEN = "isFullscreen";
static final String BUNDLE_ID = "id";
static final String BUNDLE_CARDLIST = "cardList";
static final String BUNDLE_TRANSITION_RIGHT = "transition_right";
final private TaskHandler mTasks = new TaskHandler();
Runnable barcodeImageGenerationFinishedCallback;
private long initTime = System.currentTimeMillis();
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (settings.useVolumeKeysForNavigation()) {
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
// Navigate to the previous card
if (initTime < (System.currentTimeMillis() - 1000)) {
prevNextCard(false);
}
return true;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
// Navigate to the next card
if (initTime < (System.currentTimeMillis() - 1000)) {
prevNextCard(true);
}
return true;
}
}
return super.onKeyDown(keyCode, event);
}
public void onMainImageTap() {
// If we're in fullscreen, leave fullscreen
if (isFullscreen) {
@@ -140,7 +112,6 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
// If the barcode is shown, switch to fullscreen layout
if (imageType == ImageType.BARCODE) {
setFullscreen(true);
return;
}
@@ -210,8 +181,8 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
private void extractIntentFields(Intent intent) {
final Bundle b = intent.getExtras();
loyaltyCardId = b != null ? b.getInt(BUNDLE_ID) : 0;
cardList = b != null ? b.getIntegerArrayList(BUNDLE_CARDLIST) : null;
loyaltyCardId = b != null ? b.getInt("id") : 0;
cardList = b != null ? b.getIntegerArrayList("cardList") : null;
Log.d(TAG, "View activity: id=" + loyaltyCardId);
}
@@ -223,13 +194,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
} else {
binding.scalerGuideline.setGuidelinePercent(0.5f * scale);
}
}
private void setScalerWidthGuideline(int zoomLevelWidth) {
float halfscale = zoomLevelWidth / 200f;
binding.scalerEndwidthguideline.setGuidelinePercent(0.5f + halfscale);
binding.scalerStartwidthguideline.setGuidelinePercent(0.5f - halfscale);
}
@Override
@@ -243,7 +208,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
return;
}
int transitionRight = incomingIntentExtras.getInt(BUNDLE_TRANSITION_RIGHT, -1);
int transitionRight = incomingIntentExtras.getInt("transition_right", -1);
if (transitionRight == 1) {
// right side transition
overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left);
@@ -256,7 +221,6 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
super.onCreate(savedInstanceState);
binding = LoyaltyCardViewLayoutBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
Utils.applyWindowInsets(binding.getRoot());
Toolbar toolbar = binding.toolbar;
setSupportActionBar(toolbar);
@@ -287,8 +251,36 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
database = new DBHelper(this).getWritableDatabase();
importURIHelper = new ImportURIHelper(this);
binding.barcodeScaler.setOnSeekBarChangeListener(setOnSeekBarChangeListenerUnifiedFunction());
binding.barcodeWidthscaler.setOnSeekBarChangeListener(setOnSeekBarChangeListenerUnifiedFunction());
binding.barcodeScaler.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (!fromUser) {
Log.d(TAG, "non user triggered onProgressChanged, ignoring, progress is " + progress);
return;
}
Log.d(TAG, "Progress is " + progress);
Log.d(TAG, "Max is " + binding.barcodeScaler.getMax());
float scale = (float) progress / (float) binding.barcodeScaler.getMax();
Log.d(TAG, "Scaling to " + scale);
loyaltyCard.zoomLevel = progress;
DBHelper.updateLoyaltyCardZoomLevel(database, loyaltyCardId, loyaltyCard.zoomLevel);
setScalerGuideline(loyaltyCard.zoomLevel);
drawMainImage(mainImageIndex, true, isFullscreen);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
rotationEnabled = true;
@@ -310,7 +302,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
binding.bottomAppBarUpdateBalanceButton.setOnClickListener(view -> showBalanceUpdateDialog());
binding.iconContainer.setOnClickListener(view -> {
if (loyaltyCard.getImageThumbnail(this) != null) {
if (Utils.retrieveCardImage(this, loyaltyCard.id, ImageLocationType.icon) != null) {
openImageInGallery(ImageType.ICON);
} else {
Toast.makeText(LoyaltyCardViewActivity.this, R.string.icon_header_click_text, Toast.LENGTH_LONG).show();
@@ -350,46 +342,6 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
});
}
private SeekBar.OnSeekBarChangeListener setOnSeekBarChangeListenerUnifiedFunction() {
return new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (!fromUser) {
Log.d(TAG, "non user triggered onProgressChanged, ignoring, progress is " + progress);
return;
}
Log.d(TAG, "Progress is " + progress);
if (seekBar.getId() == binding.barcodeScaler.getId()) {
Log.d(TAG, "Max is " + binding.barcodeScaler.getMax());
float scale = (float) progress / (float) binding.barcodeScaler.getMax();
Log.d(TAG, "Scaling to " + scale);
}
else {
Log.d(TAG, "Max is " + binding.barcodeWidthscaler.getMax());
float scale = (float) progress / (float) binding.barcodeWidthscaler.getMax();
Log.d(TAG, "Scaling to " + scale);
}
if (seekBar.getId() == binding.barcodeScaler.getId()) {
loyaltyCard.zoomLevel = progress;
setScalerGuideline(loyaltyCard.zoomLevel);
}
else {
loyaltyCard.zoomLevelWidth = progress;
setScalerWidthGuideline(loyaltyCard.zoomLevelWidth);
}
DBHelper.updateLoyaltyCardZoomLevel(database, loyaltyCardId, loyaltyCard.zoomLevel, loyaltyCard.zoomLevelWidth);
drawMainImage(mainImageIndex, true, isFullscreen);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
};
}
private SpannableStringBuilder padSpannableString(SpannableStringBuilder spannableStringBuilder) {
if (spannableStringBuilder.length() > 0) {
spannableStringBuilder.append("\n\n");
@@ -620,8 +572,8 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
// Restart activity with new card id and index
Intent intent = getIntent();
Bundle b = intent.getExtras();
b.putInt(BUNDLE_ID, loyaltyCardId);
b.putInt(BUNDLE_TRANSITION_RIGHT, transitionRight ? 1 : 0);
b.putInt("id", loyaltyCardId);
b.putInt("transition_right", transitionRight ? 1 : 0);
intent.putExtras(b);
intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
@@ -681,7 +633,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
window.setAttributes(attributes);
}
loyaltyCard = DBHelper.getLoyaltyCard(this, database, loyaltyCardId);
loyaltyCard = DBHelper.getLoyaltyCard(database, loyaltyCardId);
if (loyaltyCard == null) {
Log.w(TAG, "Could not lookup loyalty card " + loyaltyCardId);
Toast.makeText(this, R.string.noCardExistsError, Toast.LENGTH_LONG).show();
@@ -728,8 +680,6 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
int darkenedColor = ColorUtils.blendARGB(backgroundHeaderColor, Color.BLACK, 0.1f);
binding.barcodeScaler.setProgressTintList(ColorStateList.valueOf(darkenedColor));
binding.barcodeScaler.setThumbTintList(ColorStateList.valueOf(darkenedColor));
binding.barcodeWidthscaler.setProgressTintList(ColorStateList.valueOf(darkenedColor));
binding.barcodeWidthscaler.setThumbTintList(ColorStateList.valueOf(darkenedColor));
// Set bottomAppBar and system navigation bar color
binding.bottomAppBar.setBackgroundColor(darkenedColor);
@@ -742,8 +692,8 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
editButtonIcon.setTint(Utils.needsDarkForeground(complementaryColor) ? Color.BLACK : Color.WHITE);
binding.fabEdit.setImageDrawable(editButtonIcon);
Bitmap icon = loyaltyCard.getImageThumbnail(this);
Utils.setIconOrTextWithBackground(this, loyaltyCard, icon, binding.iconImage, binding.iconText, 1);
Bitmap icon = Utils.retrieveCardImage(this, loyaltyCard.id, ImageLocationType.icon);
Utils.setIconOrTextWithBackground(this, loyaltyCard, icon, binding.iconImage, binding.iconText);
// If the background is very bright, we should use dark icons
backgroundNeedsDarkIcons = Utils.needsDarkForeground(backgroundHeaderColor);
@@ -771,12 +721,12 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
imageTypes.add(ImageType.BARCODE);
}
frontImageBitmap = loyaltyCard.getImageFront(this);
frontImageBitmap = Utils.retrieveCardImage(this, loyaltyCard.id, ImageLocationType.front);
if (frontImageBitmap != null) {
imageTypes.add(ImageType.IMAGE_FRONT);
}
backImageBitmap = loyaltyCard.getImageBack(this);
backImageBitmap = Utils.retrieveCardImage(this, loyaltyCard.id, ImageLocationType.back);
if (backImageBitmap != null) {
imageTypes.add(ImageType.IMAGE_BACK);
}
@@ -880,8 +830,6 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
} else if (id == R.id.action_star_unstar) {
DBHelper.updateLoyaltyCardStarStatus(database, loyaltyCardId, loyaltyCard.starStatus == 0 ? 1 : 0);
new ListWidget().updateAll(LoyaltyCardViewActivity.this);
// Re-init loyaltyCard with new data from DB
onResume();
invalidateOptionsMenu();
@@ -892,7 +840,6 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
Toast.makeText(LoyaltyCardViewActivity.this, R.string.archived, Toast.LENGTH_LONG).show();
ShortcutHelper.removeShortcut(LoyaltyCardViewActivity.this, loyaltyCardId);
new ListWidget().updateAll(LoyaltyCardViewActivity.this);
// Re-init loyaltyCard with new data from DB
onResume();
@@ -918,7 +865,6 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
DBHelper.deleteLoyaltyCard(database, LoyaltyCardViewActivity.this, loyaltyCardId);
ShortcutHelper.removeShortcut(LoyaltyCardViewActivity.this, loyaltyCardId);
new ListWidget().updateAll(LoyaltyCardViewActivity.this);
finish();
dialog.dismiss();
@@ -962,8 +908,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
null,
false,
this,
addPadding,
isFullscreen);
addPadding);
mTasks.executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter);
}
}
@@ -1157,18 +1102,11 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
binding.container.setVisibility(View.GONE);
binding.fullscreenLayout.setVisibility(View.VISIBLE);
// Only show width slider if the barcode isn't square (square barcodes will resize height and width together)
// or if the internals of the barcode are squares, like DATA_MATRIX
binding.setWidthLayout.setVisibility((format.isSquare() || format.format() == BarcodeFormat.DATA_MATRIX) ? View.GONE : View.VISIBLE);
drawMainImage(mainImageIndex, true, isFullscreen);
binding.barcodeScaler.setProgress(loyaltyCard.zoomLevel);
setScalerGuideline(loyaltyCard.zoomLevel);
binding.barcodeWidthscaler.setProgress(loyaltyCard.zoomLevelWidth);
setScalerWidthGuideline(loyaltyCard.zoomLevelWidth);
// Hide actionbar
if (actionBar != null) {
actionBar.hide();

View File

@@ -2,15 +2,11 @@ package protect.card_locker;
import android.app.Activity;
import android.app.SearchManager;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.CursorIndexOutOfBoundsException;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.Log;
@@ -25,30 +21,34 @@ import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.SearchView;
import androidx.core.splashscreen.SplashScreen;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.work.Data;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.tabs.TabLayout;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import protect.card_locker.databinding.ContentMainBinding;
import protect.card_locker.databinding.MainActivityBinding;
import protect.card_locker.databinding.SortingOptionBinding;
import protect.card_locker.preferences.Settings;
import protect.card_locker.importexport.DataFormat;
import protect.card_locker.importexport.ImportExportWorker;
import protect.card_locker.preferences.SettingsActivity;
public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCardCursorAdapter.CardAdapterListener {
@@ -58,7 +58,6 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
public static final String RESTART_ACTIVITY_INTENT = "restart_activity_intent";
private static final int MEDIUM_SCALE_FACTOR_DIP = 460;
static final String STATE_SEARCH_QUERY = "SEARCH_QUERY";
private SQLiteDatabase mDatabase;
private LoyaltyCardCursorAdapter mAdapter;
@@ -66,8 +65,6 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
private SearchView mSearchView;
private int mLoyaltyCardCount = 0;
protected String mFilter = "";
private String currentQuery = "";
private String finalQuery = "";
protected Object mGroup = null;
protected DBHelper.LoyaltyCardOrder mOrder = DBHelper.LoyaltyCardOrder.Alpha;
protected DBHelper.LoyaltyCardOrderDirection mOrderDirection = DBHelper.LoyaltyCardOrderDirection.Ascending;
@@ -77,9 +74,12 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
private View mNoMatchingCardsText;
private View mNoGroupCardsText;
private TabLayout groupsTabLayout;
private Runnable mUpdateLoyaltyCardListRunnable;
private ActivityResultLauncher<Intent> mBarcodeScannerLauncher;
private ActivityResultLauncher<Intent> mSettingsLauncher;
private ActivityResultLauncher<Intent> mImportExportLauncher;
private ActionMode.Callback mCurrentActionModeCallback = new ActionMode.Callback() {
@Override
@@ -205,33 +205,11 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
SplashScreen.installSplashScreen(this);
super.onCreate(inputSavedInstanceState);
// Delete old cache files
// These could be temporary images for the cropper, temporary images in LoyaltyCard toBundle/writeParcel/ etc.
new Thread(() -> {
long twentyFourHoursAgo = System.currentTimeMillis() - (1000 * 60 * 60 * 24);
File[] tempFiles = getCacheDir().listFiles();
if (tempFiles == null) {
Log.e(TAG, "getCacheDir().listFiles() somehow returned null, this should never happen... Skipping cache cleanup...");
return;
}
for (File file : tempFiles) {
if (file.lastModified() < twentyFourHoursAgo) {
if (!file.delete()) {
Log.w(TAG, "Failed to delete cache file " + file.getPath());
}
};
}
}).start();
// We should extract the share intent after we called the super.onCreate as it may need to spawn a dialog window and the app needs to be initialized to not crash
extractIntentFields(getIntent());
binding = MainActivityBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
Utils.applyWindowInsets(binding.getRoot());
setSupportActionBar(binding.toolbar);
groupsTabLayout = binding.groups;
contentMainBinding = ContentMainBinding.bind(binding.include.getRoot());
@@ -278,15 +256,52 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
mCardList.setAdapter(mAdapter);
registerForContextMenu(mCardList);
mGroup = null;
updateLoyaltyCardList(true);
/*
* This was added for Huawei, but Huawei is just too much of a fucking pain.
* Just leaving this commented out if needed for the future idk
* https://twitter.com/SylvieLorxu/status/1379437902741012483
*
// Show privacy policy on first run
SharedPreferences privacyPolicyShownPref = getApplicationContext().getSharedPreferences(
getString(R.string.sharedpreference_privacy_policy_shown),
Context.MODE_PRIVATE);
if (privacyPolicyShownPref.getInt(getString(R.string.sharedpreference_privacy_policy_shown), 0) == 0) {
SharedPreferences.Editor privacyPolicyShownPrefEditor = privacyPolicyShownPref.edit();
privacyPolicyShownPrefEditor.putInt(getString(R.string.sharedpreference_privacy_policy_shown), 1);
privacyPolicyShownPrefEditor.apply();
new AlertDialog.Builder(this)
.setTitle(R.string.privacy_policy)
.setMessage(R.string.privacy_policy_popup_text)
.setPositiveButton(R.string.accept, null)
.setNegativeButton(R.string.privacy_policy, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
openPrivacyPolicy();
}
})
.setIcon(android.R.drawable.ic_dialog_info)
.show();
}
*/
mBarcodeScannerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
// Exit early if the user cancelled the scan (pressed back/home)
if (result.getResultCode() != RESULT_OK) {
return;
}
Intent editIntent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class);
editIntent.putExtras(result.getData().getExtras());
startActivity(editIntent);
Intent intent = result.getData();
List<BarcodeValues> barcodeValuesList = Utils.parseSetBarcodeActivityResult(Utils.BARCODE_SCAN, result.getResultCode(), intent, this);
Bundle inputBundle = intent.getExtras();
String group = inputBundle != null ? inputBundle.getString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP) : null;
processBarcodeValuesList(barcodeValuesList, group, false);
});
mSettingsLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
@@ -298,6 +313,69 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
}
});
mImportExportLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
// User didn't ask for import or export
if (result.getResultCode() != RESULT_OK) {
return;
}
// Watch for active imports/exports
new Thread(() -> {
WorkManager workManager = WorkManager.getInstance(MainActivity.this);
Snackbar importRunning = Snackbar.make(binding.getRoot(), R.string.importing, Snackbar.LENGTH_INDEFINITE);
while (true) {
try {
List<WorkInfo> activeImports = workManager.getWorkInfosForUniqueWork(ImportExportWorker.ACTION_IMPORT).get();
// We should only have one import running at a time, so it should be safe to always grab the latest
WorkInfo activeImport = activeImports.get(activeImports.size() - 1);
WorkInfo.State importState = activeImport.getState();
if (importState == WorkInfo.State.RUNNING || importState == WorkInfo.State.ENQUEUED || importState == WorkInfo.State.BLOCKED) {
importRunning.show();
} else if (importState == WorkInfo.State.SUCCEEDED) {
importRunning.dismiss();
runOnUiThread(() -> {
Toast.makeText(getApplicationContext(), getString(R.string.importSuccessful), Toast.LENGTH_LONG).show();
updateLoyaltyCardList(true);
});
break;
} else {
importRunning.dismiss();
Data outputData = activeImport.getOutputData();
// FIXME: This dialog will asynchronously be accepted or declined and we don't know the status of it so we can't show the import state
// We want to get back into this function
// A cheap fix would be to keep looping but if the user dismissed the dialog that could mean we're looping forever...
if (Objects.equals(outputData.getString(ImportExportWorker.OUTPUT_ERROR_REASON), ImportExportWorker.ERROR_PASSWORD_REQUIRED)) {
runOnUiThread(() -> ImportExportActivity.retryWithPassword(
MainActivity.this,
DataFormat.valueOf(outputData.getString(ImportExportWorker.INPUT_FORMAT)),
Uri.parse(outputData.getString(ImportExportWorker.INPUT_URI))
));
} else {
runOnUiThread(() -> {
Toast.makeText(getApplicationContext(), getString(R.string.importFailed), Toast.LENGTH_LONG).show();
Toast.makeText(getApplicationContext(), activeImport.getOutputData().getString(ImportExportWorker.OUTPUT_ERROR_REASON), Toast.LENGTH_LONG).show();
Toast.makeText(getApplicationContext(), activeImport.getOutputData().getString(ImportExportWorker.OUTPUT_ERROR_DETAILS), Toast.LENGTH_LONG).show();
});
}
break;
}
} catch (ExecutionException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
});
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
@@ -322,6 +400,7 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
if (mSearchView != null && !mSearchView.isIconified()) {
mFilter = mSearchView.getQuery().toString();
}
// Start of active tab logic
updateTabGroups(groupsTabLayout);
@@ -332,8 +411,22 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
selectedTab = activeTabPref.getInt(getString(R.string.sharedpreference_active_tab), 0);
// Restore sort preferences from Shared Preferences
mOrder = Utils.getLoyaltyCardOrder(this);
mOrderDirection = Utils.getLoyaltyCardOrderDirection(this);
// If one of the sorting prefererences has never been set or is set to an invalid value,
// stick to the defaults.
SharedPreferences sortPref = getApplicationContext().getSharedPreferences(
getString(R.string.sharedpreference_sort),
Context.MODE_PRIVATE);
String orderString = sortPref.getString(getString(R.string.sharedpreference_sort_order), null);
String orderDirectionString = sortPref.getString(getString(R.string.sharedpreference_sort_direction), null);
if (orderString != null && orderDirectionString != null) {
try {
mOrder = DBHelper.LoyaltyCardOrder.valueOf(orderString);
mOrderDirection = DBHelper.LoyaltyCardOrderDirection.valueOf(orderDirectionString);
} catch (IllegalArgumentException ignored) {
}
}
mGroup = null;
@@ -365,12 +458,6 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
mBarcodeScannerLauncher.launch(intent);
});
addButton.bringToFront();
var layoutManager = (GridLayoutManager) mCardList.getLayoutManager();
if (layoutManager != null) {
var settings = new Settings(this);
layoutManager.setSpanCount(settings.getPreferredColumnCount());
}
}
private void displayCardSetupOptions(Menu menu, boolean shouldShow) {
@@ -430,25 +517,25 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
if (mCurrentActionMode != null) {
mCurrentActionMode.finish();
}
new ListWidget().updateAll(mAdapter.mContext);
}
private void processParseResultList(List<ParseResult> parseResultList, String group, boolean closeAppOnNoBarcode) {
if (parseResultList.isEmpty()) {
throw new IllegalArgumentException("parseResultList may not be empty");
private void processBarcodeValuesList(List<BarcodeValues> barcodeValuesList, String group, boolean closeAppOnNoBarcode) {
if (barcodeValuesList.isEmpty()) {
throw new IllegalArgumentException("barcodesValues may not be empty");
}
Utils.makeUserChooseParseResultFromList(MainActivity.this, parseResultList, new ParseResultListDisambiguatorCallback() {
Utils.makeUserChooseBarcodeFromList(MainActivity.this, barcodeValuesList, new BarcodeValuesListDisambiguatorCallback() {
@Override
public void onUserChoseParseResult(ParseResult parseResult) {
Intent intent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class);
Bundle bundle = parseResult.toLoyaltyCardBundle(MainActivity.this);
public void onUserChoseBarcode(BarcodeValues barcodeValues) {
Intent newIntent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class);
Bundle newBundle = new Bundle();
newBundle.putString(LoyaltyCardEditActivity.BUNDLE_BARCODETYPE, barcodeValues.format());
newBundle.putString(LoyaltyCardEditActivity.BUNDLE_CARDID, barcodeValues.content());
if (group != null) {
bundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, group);
newBundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, group);
}
intent.putExtras(bundle);
startActivity(intent);
newIntent.putExtras(newBundle);
startActivity(newIntent);
}
@Override
@@ -464,53 +551,28 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
String receivedAction = intent.getAction();
String receivedType = intent.getType();
if (receivedAction == null || receivedType == null) {
return;
}
// Check if an image or file was shared to us
if (Intent.ACTION_SEND.equals(receivedAction)) {
List<BarcodeValues> barcodeValuesList;
List<ParseResult> parseResultList;
// Check for shared text
if (receivedAction.equals(Intent.ACTION_SEND) && receivedType.equals("text/plain")) {
LoyaltyCard loyaltyCard = new LoyaltyCard();
loyaltyCard.setCardId(intent.getStringExtra(Intent.EXTRA_TEXT));
parseResultList = Collections.singletonList(new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard));
} else {
// Parse whatever file was sent, regardless of opening or sharing
Uri data;
if (receivedAction.equals(Intent.ACTION_VIEW)) {
data = intent.getData();
} else if (receivedAction.equals(Intent.ACTION_SEND)) {
data = intent.getParcelableExtra(Intent.EXTRA_STREAM);
} else {
Log.e(TAG, "Wrong action type to parse intent");
return;
}
if (receivedType.startsWith("image/")) {
parseResultList = Utils.retrieveBarcodesFromImage(this, data);
if (receivedType.equals("text/plain")) {
barcodeValuesList = Collections.singletonList(new BarcodeValues(null, intent.getStringExtra(Intent.EXTRA_TEXT)));
} else if (receivedType.startsWith("image/")) {
barcodeValuesList = Utils.retrieveBarcodesFromImage(this, intent.getParcelableExtra(Intent.EXTRA_STREAM));
} else if (receivedType.equals("application/pdf")) {
parseResultList = Utils.retrieveBarcodesFromPdf(this, data);
} else if (Arrays.asList("application/vnd.apple.pkpass", "application/vnd-com.apple.pkpass").contains(receivedType)) {
parseResultList = Utils.retrieveBarcodesFromPkPass(this, data);
} else if (receivedType.equals("application/vnd.espass-espass")) {
// FIXME: espass is not pkpass
// However, several users stated in https://github.com/CatimaLoyalty/Android/issues/2197 that the formats are extremely similar to the point they could rename an .espass file to .pkpass and have it imported
// So it makes sense to "unofficially" treat it as a PKPASS for now, even though not completely correct
parseResultList = Utils.retrieveBarcodesFromPkPass(this, data);
barcodeValuesList = Utils.retrieveBarcodesFromPdf(this, intent.getParcelableExtra(Intent.EXTRA_STREAM));
} else {
Log.e(TAG, "Wrong mime-type");
return;
}
}
// Give up if we should parse but there is nothing to parse
if (parseResultList == null || parseResultList.isEmpty()) {
finish();
return;
}
if (barcodeValuesList.isEmpty()) {
finish();
return;
}
processParseResultList(parseResultList, null, true);
processBarcodeValuesList(barcodeValuesList, null, true);
}
}
private void extractIntentFields(Intent intent) {
@@ -544,24 +606,6 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
}
@Override
// Saving currentQuery to finalQuery for user, this will be used to restore search history, happens when user clicks a card from list
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
finalQuery = currentQuery;
// Putting the query also into outState for later use in onRestoreInstanceState when rotating screen
if (mSearchView != null) {
outState.putString(STATE_SEARCH_QUERY, finalQuery);
}
}
@Override
// Restoring instance state when rotation of screen happens with the goal to restore search query for user
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
finalQuery = savedInstanceState.getString(STATE_SEARCH_QUERY, "");
}
@Override
public boolean onCreateOptionsMenu(Menu inputMenu) {
getMenuInflater().inflate(R.menu.main_menu, inputMenu);
@@ -570,42 +614,15 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
if (searchManager != null) {
MenuItem searchMenuItem = inputMenu.findItem(R.id.action_search);
mSearchView = (SearchView) searchMenuItem.getActionView();
mSearchView = (SearchView) inputMenu.findItem(R.id.action_search).getActionView();
mSearchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
mSearchView.setSubmitButtonEnabled(false);
mSearchView.setOnCloseListener(() -> {
invalidateOptionsMenu();
return false;
});
/*
* On Android 13 and later, pressing Back while the search view is open hides the keyboard
* and collapses the search view at the same time.
* This brings back the old behavior on Android 12 and lower: pressing Back once
* hides the keyboard, press again while keyboard is hidden to collapse the search view.
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
searchMenuItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
@Override
public boolean onMenuItemActionExpand(@NonNull MenuItem item) {
return true;
}
@Override
public boolean onMenuItemActionCollapse(@NonNull MenuItem item) {
if (mSearchView.hasFocus()) {
mSearchView.clearFocus();
return false;
}
currentQuery = "";
mFilter = "";
updateLoyaltyCardList(false);
return true;
}
});
}
mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
@@ -615,21 +632,7 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
@Override
public boolean onQueryTextChange(String newText) {
mFilter = newText;
// New logic to ensure search history after coming back from picked card - user will see the last search query
if (newText.isEmpty()) {
if(!finalQuery.isEmpty()){
// Setting the query text for user after coming back from picked card from finalQuery
mSearchView.setQuery(finalQuery, false);
}
else if(!currentQuery.isEmpty()){
// Else if is needed in case user deletes search - expected behaviour is to show all cards
currentQuery = "";
mSearchView.setQuery(currentQuery, false);
}
} else {
// Setting search query each time user changes the text in search to temporary variable to be used later in finalQuery String which will be used to restore search history
currentQuery = newText;
}
TabLayout.Tab currentTab = groupsTabLayout.getTabAt(groupsTabLayout.getSelectedTabPosition());
mGroup = currentTab != null ? currentTab.getTag() : null;
@@ -638,14 +641,6 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
return true;
}
});
// Check if we came from a picked card back to search, in that case we want to show the search view with previous search query
if(!finalQuery.isEmpty()){
// Expand the search view to show the query
searchMenuItem.expandActionView();
// Setting the query text to empty String due to behaviour of onQueryTextChange after coming back from picked card - onQueryTextChange is called automatically without users interaction
finalQuery = "";
mSearchView.setQuery(currentQuery, false);
}
}
return super.onCreateOptionsMenu(inputMenu);
@@ -699,8 +694,6 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
showReversed.isChecked() ? DBHelper.LoyaltyCardOrderDirection.Descending : DBHelper.LoyaltyCardOrderDirection.Ascending
);
new ListWidget().updateAll(this);
dialog.dismiss();
});
@@ -720,7 +713,7 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
if (id == R.id.action_import_export) {
Intent i = new Intent(getApplicationContext(), ImportExportActivity.class);
startActivity(i);
mImportExportLauncher.launch(i);
return true;
}
@@ -864,14 +857,14 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
Intent intent = new Intent(this, LoyaltyCardViewActivity.class);
intent.setAction("");
final Bundle b = new Bundle();
b.putInt(LoyaltyCardViewActivity.BUNDLE_ID, loyaltyCard.id);
b.putInt("id", loyaltyCard.id);
ArrayList<Integer> cardList = new ArrayList<>();
for (int i = 0; i < mAdapter.getItemCount(); i++) {
cardList.add(mAdapter.getCard(i).id);
}
b.putIntegerArrayList(LoyaltyCardViewActivity.BUNDLE_CARDLIST, cardList);
b.putIntegerArrayList("cardList", cardList);
intent.putExtras(b);
startActivity(intent);

View File

@@ -48,7 +48,6 @@ public class ManageGroupActivity extends CatimaAppCompatActivity implements Mana
super.onCreate(inputSavedInstanceState);
binding = ActivityManageGroupBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
Utils.applyWindowInsetsAndFabOffset(binding.getRoot(), binding.fabSave);
Toolbar toolbar = binding.toolbar;
setSupportActionBar(toolbar);

View File

@@ -33,7 +33,7 @@ public class ManageGroupCursorAdapter extends LoyaltyCardCursorAdapter {
@Override
public void onBindViewHolder(LoyaltyCardListItemViewHolder inputHolder, Cursor inputCursor) {
LoyaltyCard loyaltyCard = LoyaltyCard.fromCursor(mContext, inputCursor);
LoyaltyCard loyaltyCard = LoyaltyCard.toLoyaltyCard(inputCursor);
Boolean overlayValue = mInGroupOverlay.get(loyaltyCard.id);
if ((overlayValue != null ? overlayValue : isLoyaltyCardInGroup(loyaltyCard.id))) {
mAnimationItemsIndex.put(inputCursor.getPosition(), true);

View File

@@ -42,7 +42,6 @@ public class ManageGroupsActivity extends CatimaAppCompatActivity implements Gro
binding = ManageGroupsActivityBinding.inflate(getLayoutInflater());
setTitle(R.string.groups);
setContentView(binding.getRoot());
Utils.applyWindowInsets(binding.getRoot());
Toolbar toolbar = binding.toolbar;
setSupportActionBar(toolbar);
enableToolbarBackButton();

View File

@@ -0,0 +1,63 @@
package protect.card_locker;
import static android.content.Context.NOTIFICATION_SERVICE;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class NotificationHelper {
// Do not change these IDs!
public static final String CHANNEL_IMPORT = "import";
public static final String CHANNEL_EXPORT = "export";
public static final int IMPORT_ID = 100;
public static final int IMPORT_PROGRESS_ID = 101;
public static final int EXPORT_ID = 103;
public static final int EXPORT_PROGRESS_ID = 104;
public static Notification.Builder createNotificationBuilder(@NonNull Context context, @NonNull String channel, @NonNull int icon, @NonNull String title, @Nullable String message) {
Notification.Builder notificationBuilder = new Notification.Builder(context)
.setSmallIcon(icon)
.setTicker(title)
.setContentTitle(title);
if (message != null) {
notificationBuilder.setContentText(message);
}
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
NotificationChannel notificationChannel = new NotificationChannel(channel, getChannelName(channel), NotificationManager.IMPORTANCE_DEFAULT);
notificationManager.createNotificationChannel(notificationChannel);
notificationBuilder.setChannelId(channel);
}
return notificationBuilder;
}
public static void sendNotification(@NonNull Context context, @NonNull int notificationId, @NonNull Notification notification) {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
notificationManager.notify(notificationId, notification);
}
private static String getChannelName(@NonNull String channel) {
switch(channel) {
case CHANNEL_IMPORT:
return "Import";
case CHANNEL_EXPORT:
return "Export";
default:
throw new IllegalArgumentException("Unknown notification channel");
}
}
}

View File

@@ -1,31 +0,0 @@
package protect.card_locker
import android.content.Context
import android.os.Bundle
class ParseResult(
val parseResultType: ParseResultType,
val loyaltyCard: LoyaltyCard) {
var note: String? = null
fun toLoyaltyCardBundle(context: Context): Bundle {
when (parseResultType) {
ParseResultType.FULL -> return loyaltyCard.toBundle(context, listOf())
ParseResultType.BARCODE_ONLY -> {
val defaultLoyaltyCard = LoyaltyCard()
defaultLoyaltyCard.setBarcodeId(null)
defaultLoyaltyCard.setBarcodeType(loyaltyCard.barcodeType)
defaultLoyaltyCard.setCardId(loyaltyCard.cardId)
return defaultLoyaltyCard.toBundle(
context,
listOf(
LoyaltyCard.BUNDLE_LOYALTY_CARD_BARCODE_ID,
LoyaltyCard.BUNDLE_LOYALTY_CARD_BARCODE_TYPE,
LoyaltyCard.BUNDLE_LOYALTY_CARD_CARD_ID
)
)
}
}
}
}

View File

@@ -1,6 +0,0 @@
package protect.card_locker;
public interface ParseResultListDisambiguatorCallback {
void onUserChoseParseResult(ParseResult parseResult);
void onUserDismissedSelector();
}

View File

@@ -1,6 +0,0 @@
package protect.card_locker
enum class ParseResultType {
FULL,
BARCODE_ONLY
}

View File

@@ -42,6 +42,16 @@ public class PermissionUtils {
return ContextCompat.checkSelfPermission(activity, android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED;
}
/**
* Check if post notifications permission is needed
*
* @param activity
* @return
*/
public static boolean needsPostNotificationsPermission(Activity activity) {
return ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED;
}
/**
* Call onRequestPermissionsResult after storage read permission was granted.
* Mocks a successful grant if a grant is not necessary.
@@ -91,4 +101,37 @@ public class PermissionUtils {
activity.onMockedRequestPermissionsResult(requestCode, permissions, mockedResults);
}
}
/**
* Call onRequestPermissionsResult after notification permission was granted.
* Mocks a successful grant if a grant is not necessary.
*
* @param activity
* @param requestCode
*/
public static void requestPostNotificationsPermission(CatimaAppCompatActivity activity, int requestCode) {
int[] mockedResults = new int[]{ PackageManager.PERMISSION_GRANTED };
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.TIRAMISU) {
String[] permissions = new String[0];
activity.onMockedRequestPermissionsResult(requestCode, permissions, mockedResults);
return;
}
String[] permissions = new String[]{ Manifest.permission.POST_NOTIFICATIONS};
if (needsPostNotificationsPermission(activity)) {
ActivityCompat.requestPermissions(activity, permissions, requestCode);
} else {
// FIXME: This points to onMockedRequestPermissionResult instead of to
// onRequestPermissionResult because onRequestPermissionResult was only introduced in
// Android 6.0 (SDK 23) and we and to support Android 5.0 (SDK 21) too.
//
// When minSdk becomes 23, this should point to onRequestPermissionResult directly and
// the activity input variable should be changed from CatimaAppCompatActivity to
// Activity.
activity.onMockedRequestPermissionsResult(requestCode, permissions, mockedResults);
}
}
}

View File

@@ -1,443 +0,0 @@
package protect.card_locker
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
import android.util.ArrayMap
import android.util.Log
import com.google.zxing.BarcodeFormat
import net.lingala.zip4j.io.inputstream.ZipInputStream
import net.lingala.zip4j.model.LocalFileHeader
import org.json.JSONException
import org.json.JSONObject
import java.io.FileNotFoundException
import java.io.IOException
import java.math.BigDecimal
import java.text.DateFormat
import java.text.ParseException
import java.time.ZonedDateTime
import java.time.format.DateTimeParseException
import java.util.Currency
import java.util.Date
class PkpassParser(context: Context, uri: Uri?) {
private var mContext = context
private var translations: ArrayMap<String, Map<String, String>> = ArrayMap()
private var passContent: JSONObject? = null
private var store: String? = null
private var note: String? = null
private var validFrom: Date? = null
private var expiry: Date? = null
private val balance: BigDecimal = BigDecimal(0)
private val balanceType: Currency? = null
// FIXME: Some cards may not have any barcodes, but Catima doesn't accept null card ID
// An empty string card ID would prevent a crash, but would be blocked in the edit activity
// Setting the default to the "No barcode" text at least prevents a crash, but it is definitely a hack
private var cardId: String = context.getString(R.string.noBarcode)
private var barcodeId: String? = null
private var barcodeType: CatimaBarcode? = null
private var headerColor: Int? = null
private val starStatus = 0
private val lastUsed: Long = 0
private val zoomLevel = DBHelper.DEFAULT_ZOOM_LEVEL
private val zoomLevelWidth = DBHelper.DEFAULT_ZOOM_LEVEL_WIDTH
private var archiveStatus = 0
var image: Bitmap? = null
private set
private var logoSize = 0
init {
if (passContent != null) {
throw IllegalStateException("Pkpass instance already initialized!")
}
mContext = context
Log.i(TAG, "Received Pkpass file")
if (uri == null) {
Log.e(TAG, "Uri did not contain any data")
throw IOException(context.getString(R.string.errorReadingFile))
}
try {
mContext.contentResolver.openInputStream(uri).use { inputStream ->
ZipInputStream(inputStream).use { zipInputStream ->
var localFileHeader: LocalFileHeader?
while (true) {
// Retrieve the next file
localFileHeader = zipInputStream.nextEntry
// If no next file, exit loop
if (localFileHeader == null) {
break
}
// Ignore directories
if (localFileHeader.isDirectory) continue
// We assume there are three options, as per spec:
// language.lproj/pass.strings
// file.extension
// More directories are ignored
val filenameParts = localFileHeader.fileName.split('/')
if (filenameParts.size > 2) {
continue
} else if (filenameParts.size == 2) {
// Doesn't seem like a language directory, ignore
if (!filenameParts[0].endsWith(".lproj")) continue
val locale = filenameParts[0].removeSuffix(".lproj")
translations[locale] = parseLanguageStrings(ZipUtils.read(zipInputStream))
}
// Not a language, parse as normal files
when (localFileHeader.fileName) {
"logo.png" -> loadImageIfBiggerSize(1, zipInputStream)
"logo@2x.png" -> loadImageIfBiggerSize(2, zipInputStream)
"logo@3x.png" -> loadImageIfBiggerSize(3, zipInputStream)
"pass.json" -> passContent = ZipUtils.readJSON(zipInputStream) // Parse this last, so we're sure we have all language info
}
}
checkNotNull(passContent) { "File lacks pass.json" }
}
}
} catch (e: FileNotFoundException) {
throw IOException(mContext.getString(R.string.errorReadingFile))
} catch (e: Exception) {
throw e
}
}
fun listLocales(): List<String> {
return translations.keys.toList()
}
fun toLoyaltyCard(locale: String?): LoyaltyCard {
parsePassJSON(checkNotNull(passContent) { "Pkpass instance not yet initialized!" }, locale)
return LoyaltyCard(
-1,
store,
note,
validFrom,
expiry,
balance,
balanceType,
cardId,
barcodeId,
barcodeType,
headerColor,
starStatus,
lastUsed,
zoomLevel,
zoomLevelWidth,
archiveStatus,
image,
null,
null,
null,
null,
null
)
}
private fun getTranslation(string: String, locale: String?): String {
if (locale == null) {
return string
}
val localeStrings = translations[locale]
return localeStrings?.get(string) ?: string
}
private fun loadImageIfBiggerSize(fileLogoSize: Int, zipInputStream: ZipInputStream) {
if (logoSize < fileLogoSize) {
image = ZipUtils.readImage(zipInputStream)
logoSize = fileLogoSize
}
}
private fun parseColor(color: String): Int? {
// First, try formats supported by Android natively
try {
return Color.parseColor(color)
} catch (ignored: IllegalArgumentException) {}
// If that didn't work, try parsing it as a rbg(0,0,255) value
val red: Int;
val green: Int;
val blue: Int;
// Parse rgb(0,0,0) string
val rgbInfo = Regex("""^rgb\(\s*(?<red>\d+)\s*,\s*(?<green>\d+)\s*,\s*(?<blue>\d+)\s*\)$""").find(color)
if (rgbInfo == null) {
return null
}
// Convert to integers
try {
red = rgbInfo.groups[1]!!.value.toInt()
green = rgbInfo.groups[2]!!.value.toInt()
blue = rgbInfo.groups[3]!!.value.toInt()
} catch (e: NumberFormatException) {
return null
}
// Ensure everything is in a valid range as Color.rgb does not do range checks
if (red < 0 || red > 255) return null
if (green < 0 || green > 255) return null
if (blue < 0 || blue > 255) return null
return Color.rgb(red, green, blue)
}
private fun parseDateTime(dateTime: String): Date {
return Date.from(ZonedDateTime.parse(dateTime).toInstant())
}
private fun parseLanguageStrings(data: String): Map<String, String> {
val output = ArrayMap<String, String>()
// Translations look like this:
// "key_name" = "Translated value";
//
// However, "Translated value" may be multiple lines and may contain " (however, it'll be escaped)
var translationLine = StringBuilder()
for (line in data.lines()) {
translationLine.append(line)
// Make sure we don't have a false ending (this is the escaped double quote: \";)
if (!line.endsWith("\\\";") and line.endsWith("\";")) {
// We reached a translation ending, time to parse it
// 1. Split into key and value
// 2. Remove cruft of each
// 3. Clean up escape sequences
val keyValue = translationLine.toString().split("=", ignoreCase = false, limit = 2)
val key = keyValue[0].trim().removePrefix("\"").removeSuffix("\"")
val value = keyValue[1].trim().removePrefix("\"").removeSuffix("\";").replace("\\", "")
output[key] = value
translationLine = StringBuilder()
} else {
translationLine.append("\n")
}
}
return output
}
private fun parsePassJSON(jsonObject: JSONObject, locale: String?) {
if (jsonObject.getInt("formatVersion") != 1) {
throw IllegalArgumentException(mContext.getString(R.string.unsupportedFile))
}
// Prefer logoText for store, it's generally shorter
try {
store = jsonObject.getString("logoText")
} catch (ignored: JSONException) {}
if (store.isNullOrEmpty()) {
store = jsonObject.getString("organizationName")
}
val noteText = StringBuilder()
noteText.append(getTranslation(jsonObject.getString("description"), locale))
try {
validFrom = parseDateTime(jsonObject.getString("relevantDate"))
} catch (ignored: JSONException) {}
try {
expiry = parseDateTime(jsonObject.getString("expirationDate"))
} catch (ignored: JSONException) {}
try {
headerColor = parseColor(jsonObject.getString("backgroundColor"))
} catch (ignored: JSONException) {}
var pkPassHasBarcodes = false
var validBarcodeFound = false
// Create a list of possible barcodes
val barcodes = ArrayList<JSONObject>()
// Append the non-deprecated entries
try {
val foundInBarcodesField = jsonObject.getJSONArray("barcodes")
for (i in 0 until foundInBarcodesField.length()) {
barcodes.add(foundInBarcodesField.getJSONObject(i))
}
} catch (ignored: JSONException) {}
// Append the deprecated entry if it exists
try {
barcodes.add(jsonObject.getJSONObject("barcode"))
} catch (ignored: JSONException) {}
for (barcode in barcodes) {
pkPassHasBarcodes = true
try {
parsePassJSONBarcodeField(barcode)
validBarcodeFound = true
break
} catch (ignored: IllegalArgumentException) {}
}
if (pkPassHasBarcodes && !validBarcodeFound) {
throw FormatException(mContext.getString(R.string.errorReadingFile))
}
// An used card being "archived" probably is the most sensible way to map "voided"
archiveStatus = try {
if (jsonObject.getBoolean("voided")) 1 else 0
} catch (ignored: JSONException) {
0
}
// Append type-specific info to the pass
// Find the relevant pass type and parse it
for (passType in listOf("boardingPass", "coupon", "eventTicket", "generic")) {
try {
var extraText = parsePassJSONPassFields(
jsonObject.getJSONObject(passType),
locale
)
noteText.append("\n\n")
noteText.append(extraText)
break
} catch (ignored: JSONException) {}
}
note = noteText.toString()
}
/* Return success or failure */
private fun parsePassJSONBarcodeField(barcodeInfo: JSONObject) {
val format = barcodeInfo.getString("format")
// We only need to check these 4 formats as no other options are valid in the PkPass spec
barcodeType = when(format) {
"PKBarcodeFormatQR" -> CatimaBarcode.fromBarcode(BarcodeFormat.QR_CODE)
"PKBarcodeFormatPDF417" -> CatimaBarcode.fromBarcode(BarcodeFormat.PDF_417)
"PKBarcodeFormatAztec" -> CatimaBarcode.fromBarcode(BarcodeFormat.AZTEC)
"PKBarcodeFormatCode128" -> CatimaBarcode.fromBarcode(BarcodeFormat.CODE_128)
else -> throw IllegalArgumentException("No valid barcode type")
}
// FIXME: We probably need to do something with the messageEncoding field
try {
cardId = barcodeInfo.getString("altText")
barcodeId = barcodeInfo.getString("message")
} catch (ignored: JSONException) {
cardId = barcodeInfo.getString("message")
barcodeId = null
}
// Don't set barcodeId if it's the same as cardId
if (cardId == barcodeId) {
barcodeId = null
}
}
private fun parsePassJSONPassFields(fieldsParent: JSONObject, locale: String?): String {
// These fields contain a lot of info on where we're supposed to display them, but Catima doesn't really have anything for that
// So for now, throw them all into the description field in a logical order
val noteContents: MutableList<String> = ArrayList()
// Collect all the groups of fields that exist
for (fieldType in listOf("headerFields", "primaryFields", "secondaryFields", "auxiliaryFields", "backFields")) {
val content = StringBuilder()
try {
val fieldArray = fieldsParent.getJSONArray(fieldType)
for (i in 0 until fieldArray.length()) {
val entry = fieldArray.getJSONObject(i)
content.append(parsePassJSONPassField(entry, locale))
// If this is not the last part, add spacing on the end
if (i < (fieldArray.length() - 1)) {
content.append("\n")
}
}
} catch (ignore: JSONException) {
} catch (ignore: ParseException) {
}
if (content.isNotEmpty()) {
noteContents.add(content.toString())
}
}
// Merge all field groups together, one paragraph for field group
val output = StringBuilder()
for (i in 0 until noteContents.size) {
output.append(noteContents[i])
// If this is not the last part, add newlines to separate
if (i < (noteContents.size - 1)) {
output.append("\n\n")
}
}
return output.toString()
}
private fun parsePassJSONPassField(field: JSONObject, locale: String?): String {
// Value may be a localizable string, a date or a number. So let's try to parse it as a date first
var value = getTranslation(field.getString("value"), locale)
try {
value = DateFormat.getDateTimeInstance().format(parseDateTime(value))
} catch (ignored: DateTimeParseException) {
// It's fine if it's not a date
}
// FIXME: Use the Android thing for formatted strings here
if (field.has("currencyCode")) {
val valueCurrency = Currency.getInstance(field.getString("currencyCode"))
value = Utils.formatBalance(
mContext,
Utils.parseBalance(value, valueCurrency),
valueCurrency
)
} else if (field.has("numberStyle")) {
if (field.getString("numberStyle") == "PKNumberStylePercent") {
// FIXME: Android formatting string
value = "${value}%"
}
}
val label = getTranslation(field.getString("label"), locale)
if (label.isNotEmpty()) {
return "$label: $value"
}
return value
}
companion object {
private const val TAG = "Catima"
}
}

View File

@@ -24,8 +24,6 @@ import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ListAdapter;
import android.widget.SimpleAdapter;
import android.widget.TextView;
import android.widget.Toast;
@@ -37,7 +35,6 @@ import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.zxing.DecodeHintType;
import com.google.zxing.ResultPoint;
import com.google.zxing.client.android.Intents;
import com.journeyapps.barcodescanner.BarcodeCallback;
@@ -45,8 +42,6 @@ import com.journeyapps.barcodescanner.BarcodeResult;
import com.journeyapps.barcodescanner.CaptureManager;
import com.journeyapps.barcodescanner.DecoratedBarcodeView;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import protect.card_locker.databinding.CustomBarcodeScannerBinding;
@@ -68,7 +63,6 @@ public class ScanActivity extends CatimaAppCompatActivity {
private static final int PERMISSION_SCAN_ADD_FROM_IMAGE = 100;
private static final int PERMISSION_SCAN_ADD_FROM_PDF = 101;
private static final int PERMISSION_SCAN_ADD_FROM_PKPASS = 102;
private CaptureManager capture;
private DecoratedBarcodeView barcodeScannerView;
@@ -81,15 +75,13 @@ public class ScanActivity extends CatimaAppCompatActivity {
// can't use the pre-made contract because that launches the file manager for image type instead of gallery
private ActivityResultLauncher<Intent> photoPickerLauncher;
private ActivityResultLauncher<Intent> pdfPickerLauncher;
private ActivityResultLauncher<Intent> pkpassPickerLauncher;
static final String STATE_SCANNER_ACTIVE = "scannerActive";
private boolean mScannerActive = true;
private boolean mHasError = false;
private void extractIntentFields(Intent intent) {
final Bundle b = intent.getExtras();
cardId = b != null ? b.getString(LoyaltyCard.BUNDLE_LOYALTY_CARD_CARD_ID) : null;
cardId = b != null ? b.getString(LoyaltyCardEditActivity.BUNDLE_CARDID) : null;
addGroup = b != null ? b.getString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP) : null;
Log.d(TAG, "Scan activity: id=" + cardId);
}
@@ -101,7 +93,6 @@ public class ScanActivity extends CatimaAppCompatActivity {
customBarcodeScannerBinding = CustomBarcodeScannerBinding.bind(binding.zxingBarcodeScanner);
setTitle(R.string.scanCardBarcode);
setContentView(binding.getRoot());
Utils.applyWindowInsets(binding.getRoot());
Toolbar toolbar = binding.toolbar;
setSupportActionBar(toolbar);
enableToolbarBackButton();
@@ -111,46 +102,18 @@ public class ScanActivity extends CatimaAppCompatActivity {
manualAddLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.SELECT_BARCODE_REQUEST, result.getResultCode(), result.getData()));
photoPickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.BARCODE_IMPORT_FROM_IMAGE_FILE, result.getResultCode(), result.getData()));
pdfPickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.BARCODE_IMPORT_FROM_PDF_FILE, result.getResultCode(), result.getData()));
pkpassPickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.BARCODE_IMPORT_FROM_PKPASS_FILE, result.getResultCode(), result.getData()));
customBarcodeScannerBinding.fabOtherOptions.setOnClickListener(view -> {
setScannerActive(false);
ArrayList<HashMap<String, Object>> list = new ArrayList<>();
String[] texts = new String[]{
getString(R.string.addWithoutBarcode),
getString(R.string.addManually),
getString(R.string.addFromImage),
getString(R.string.addFromPdfFile),
getString(R.string.addFromPkpass)
};
Object[] icons = new Object[]{
R.drawable.baseline_block_24,
R.drawable.ic_edit,
R.drawable.baseline_image_24,
R.drawable.baseline_picture_as_pdf_24,
R.drawable.local_activity_24px
};
String[] columns = new String[]{"text", "icon"};
for (int i = 0; i < texts.length; i++) {
HashMap<String, Object> map = new HashMap<>();
map.put(columns[0], texts[i]);
map.put(columns[1], icons[i]);
list.add(map);
}
ListAdapter adapter = new SimpleAdapter(
ScanActivity.this,
list,
R.layout.alertdialog_row_with_icon,
columns,
new int[]{R.id.textView, R.id.imageView}
);
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(ScanActivity.this);
builder.setTitle(getString(R.string.add_a_card_in_a_different_way));
builder.setAdapter(
adapter,
builder.setItems(
new CharSequence[]{
getString(R.string.addWithoutBarcode),
getString(R.string.addManually),
getString(R.string.addFromImage),
getString(R.string.addFromPdfFile)
},
(dialogInterface, i) -> {
switch (i) {
case 0:
@@ -163,10 +126,7 @@ public class ScanActivity extends CatimaAppCompatActivity {
addFromImage();
break;
case 3:
addFromPdf();
break;
case 4:
addFromPkPass();
addFromPdfFile();
break;
default:
throw new IllegalArgumentException("Unknown 'Add a card in a different way' dialog option");
@@ -177,17 +137,11 @@ public class ScanActivity extends CatimaAppCompatActivity {
builder.show();
});
// Configure barcodeScanner
barcodeScannerView = binding.zxingBarcodeScanner;
Intent barcodeScannerIntent = new Intent();
Bundle barcodeScannerIntentBundle = new Bundle();
barcodeScannerIntentBundle.putBoolean(DecodeHintType.ALSO_INVERTED.name(), Boolean.TRUE);
barcodeScannerIntent.putExtras(barcodeScannerIntentBundle);
barcodeScannerView.initializeFromIntent(barcodeScannerIntent);
// Even though we do the actual decoding with the barcodeScannerView
// CaptureManager needs to be running to show the camera and scanning bar
capture = new CatimaCaptureManager(this, barcodeScannerView, this::onCaptureManagerError);
capture = new CatimaCaptureManager(this, barcodeScannerView);
Intent captureIntent = new Intent();
Bundle captureIntentBundle = new Bundle();
captureIntentBundle.putBoolean(Intents.Scan.BEEP_ENABLED, false);
@@ -197,11 +151,16 @@ public class ScanActivity extends CatimaAppCompatActivity {
barcodeScannerView.decodeSingle(new BarcodeCallback() {
@Override
public void barcodeResult(BarcodeResult result) {
LoyaltyCard loyaltyCard = new LoyaltyCard();
loyaltyCard.setCardId(result.getText());
loyaltyCard.setBarcodeType(CatimaBarcode.fromBarcode(result.getBarcodeFormat()));
returnResult(new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard));
Intent scanResult = new Intent();
Bundle scanResultBundle = new Bundle();
scanResultBundle.putString(BARCODE_CONTENTS, result.getText());
scanResultBundle.putString(BARCODE_FORMAT, result.getBarcodeFormat().name());
if (addGroup != null) {
scanResultBundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, addGroup);
}
scanResult.putExtras(scanResultBundle);
ScanActivity.this.setResult(RESULT_OK, scanResult);
finish();
}
@Override
@@ -219,14 +178,9 @@ public class ScanActivity extends CatimaAppCompatActivity {
capture.onResume();
}
if (!Utils.deviceHasCamera(this)) {
showCameraError(getString(R.string.noCameraFoundGuideText), false);
} else if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
showCameraPermissionMissingText();
} else {
hideCameraError();
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
showCameraPermissionMissingText(false);
}
scaleScreen();
}
@@ -305,31 +259,33 @@ public class ScanActivity extends CatimaAppCompatActivity {
mScannerActive = isActive;
}
private void returnResult(ParseResult parseResult) {
Intent result = new Intent();
Bundle bundle = parseResult.toLoyaltyCardBundle(ScanActivity.this);
private void returnResult(String barcodeContents, String barcodeFormat) {
Intent manualResult = new Intent();
Bundle manualResultBundle = new Bundle();
manualResultBundle.putString(BARCODE_CONTENTS, barcodeContents);
manualResultBundle.putString(BARCODE_FORMAT, barcodeFormat);
if (addGroup != null) {
bundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, addGroup);
manualResultBundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, addGroup);
}
result.putExtras(bundle);
ScanActivity.this.setResult(RESULT_OK, result);
manualResult.putExtras(manualResultBundle);
ScanActivity.this.setResult(RESULT_OK, manualResult);
finish();
}
private void handleActivityResult(int requestCode, int resultCode, Intent intent) {
super.onActivityResult(requestCode, resultCode, intent);
List<ParseResult> parseResultList = Utils.parseSetBarcodeActivityResult(requestCode, resultCode, intent, this);
List<BarcodeValues> barcodeValuesList = Utils.parseSetBarcodeActivityResult(requestCode, resultCode, intent, this);
if (parseResultList.isEmpty()) {
if (barcodeValuesList.isEmpty()) {
setScannerActive(true);
return;
}
Utils.makeUserChooseParseResultFromList(this, parseResultList, new ParseResultListDisambiguatorCallback() {
Utils.makeUserChooseBarcodeFromList(this, barcodeValuesList, new BarcodeValuesListDisambiguatorCallback() {
@Override
public void onUserChoseParseResult(ParseResult parseResult) {
returnResult(parseResult);
public void onUserChoseBarcode(BarcodeValues barcodeValues) {
returnResult(barcodeValues.content(), barcodeValues.format());
}
@Override
@@ -376,9 +332,7 @@ public class ScanActivity extends CatimaAppCompatActivity {
// Buttons
builder.setPositiveButton(getString(R.string.ok), (dialog, which) -> {
LoyaltyCard loyaltyCard = new LoyaltyCard();
loyaltyCard.setCardId(input.getText().toString());
returnResult(new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard));
returnResult(input.getText().toString(), "");
});
builder.setNegativeButton(getString(R.string.cancel), (dialog, which) -> dialog.cancel());
AlertDialog dialog = builder.create();
@@ -413,7 +367,7 @@ public class ScanActivity extends CatimaAppCompatActivity {
Intent i = new Intent(getApplicationContext(), BarcodeSelectorActivity.class);
if (cardId != null) {
final Bundle b = new Bundle();
b.putString(LoyaltyCard.BUNDLE_LOYALTY_CARD_CARD_ID, cardId);
b.putString("initialCardId", cardId);
i.putExtras(b);
}
manualAddLauncher.launch(i);
@@ -427,14 +381,10 @@ public class ScanActivity extends CatimaAppCompatActivity {
PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_IMAGE);
}
public void addFromPdf() {
public void addFromPdfFile() {
PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_PDF);
}
public void addFromPkPass() {
PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_PKPASS);
}
private void addFromImageOrFileAfterPermission(String mimeType, ActivityResultLauncher<Intent> launcher, int chooserText, int errorMessage) {
Intent photoPickerIntent = new Intent(Intent.ACTION_PICK);
photoPickerIntent.setType(mimeType);
@@ -452,37 +402,12 @@ public class ScanActivity extends CatimaAppCompatActivity {
}
}
public void onCaptureManagerError(String errorMessage) {
if (mHasError) {
// We're already showing an error, ignore this new error
return;
}
showCameraError(errorMessage, false);
}
private void showCameraPermissionMissingText() {
showCameraError(getString(R.string.noCameraPermissionDirectToSystemSetting), true);
}
private void showCameraError(String message, boolean setOnClick) {
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorMessage.setText(message);
setCameraErrorState(true, setOnClick);
}
private void hideCameraError() {
setCameraErrorState(false, false);
}
private void setCameraErrorState(boolean visible, boolean setOnClick) {
mHasError = visible;
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorClickableArea.setOnClickListener(visible && setOnClick ? v -> {
private void showCameraPermissionMissingText(boolean show) {
customBarcodeScannerBinding.cameraPermissionDeniedLayout.cameraPermissionDeniedClickableArea.setOnClickListener(show ? v -> {
navigateToSystemPermissionSetting();
} : null);
customBarcodeScannerBinding.cardInputContainer.setBackgroundColor(visible ? obtainThemeAttribute(com.google.android.material.R.attr.colorSurface) : Color.TRANSPARENT);
customBarcodeScannerBinding.cameraErrorLayout.getRoot().setVisibility(visible ? View.VISIBLE : View.GONE);
customBarcodeScannerBinding.cardInputContainer.setBackgroundColor(show ? obtainThemeAttribute(com.google.android.material.R.attr.colorSurface) : Color.TRANSPARENT);
customBarcodeScannerBinding.cameraPermissionDeniedLayout.getRoot().setVisibility(show ? View.VISIBLE : View.GONE);
}
private void scaleScreen() {
@@ -492,8 +417,8 @@ public class ScanActivity extends CatimaAppCompatActivity {
float mediumSizePx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,MEDIUM_SCALE_FACTOR_DIP,getResources().getDisplayMetrics());
boolean shouldScaleSmaller = screenHeight < mediumSizePx;
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorIcon.setVisibility(shouldScaleSmaller ? View.GONE : View.VISIBLE);
customBarcodeScannerBinding.cameraErrorLayout.cameraErrorTitle.setVisibility(shouldScaleSmaller ? View.GONE : View.VISIBLE);
customBarcodeScannerBinding.cameraPermissionDeniedLayout.cameraPermissionDeniedIcon.setVisibility(shouldScaleSmaller ? View.GONE : View.VISIBLE);
customBarcodeScannerBinding.cameraPermissionDeniedLayout.cameraPermissionDeniedTitle.setVisibility(shouldScaleSmaller ? View.GONE : View.VISIBLE);
}
private int obtainThemeAttribute(int attribute) {
@@ -519,19 +444,13 @@ public class ScanActivity extends CatimaAppCompatActivity {
boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
if (requestCode == CaptureManager.getCameraPermissionReqCode()) {
if (granted) {
hideCameraError();
} else {
showCameraPermissionMissingText();
}
} else if (requestCode == PERMISSION_SCAN_ADD_FROM_IMAGE || requestCode == PERMISSION_SCAN_ADD_FROM_PDF || requestCode == PERMISSION_SCAN_ADD_FROM_PKPASS) {
showCameraPermissionMissingText(!granted);
} else if (requestCode == PERMISSION_SCAN_ADD_FROM_IMAGE || requestCode == PERMISSION_SCAN_ADD_FROM_PDF) {
if (granted) {
if (requestCode == PERMISSION_SCAN_ADD_FROM_IMAGE) {
addFromImageOrFileAfterPermission("image/*", photoPickerLauncher, R.string.addFromImage, R.string.failedLaunchingPhotoPicker);
} else if (requestCode == PERMISSION_SCAN_ADD_FROM_PDF) {
addFromImageOrFileAfterPermission("application/pdf", pdfPickerLauncher, R.string.addFromPdfFile, R.string.failedLaunchingFileManager);
} else {
addFromImageOrFileAfterPermission("application/*", pkpassPickerLauncher, R.string.addFromPkpass, R.string.failedLaunchingFileManager);
addFromImageOrFileAfterPermission("application/pdf", pdfPickerLauncher, R.string.addFromPdfFile, R.string.failedLaunchingFileManager);
}
} else {
setScannerActive(true);

View File

@@ -33,6 +33,7 @@ class ShortcutHelper {
private static final int ADAPTIVE_BITMAP_SIZE = 108 * ADAPTIVE_BITMAP_SCALE;
private static final int ADAPTIVE_BITMAP_VISIBLE_SIZE = 72 * ADAPTIVE_BITMAP_SCALE;
private static final int ADAPTIVE_BITMAP_IMAGE_SIZE = ADAPTIVE_BITMAP_VISIBLE_SIZE + 5 * ADAPTIVE_BITMAP_SCALE;
private static final int PADDING_COLOR_OVERLAY = Color.argb(127, 0, 0, 0);
/**
* Add a card to the app shortcuts, and maintain a list of the most
@@ -86,7 +87,7 @@ class ShortcutHelper {
for (int index = 0; index < list.size(); index++) {
ShortcutInfoCompat prevShortcut = list.get(index);
LoyaltyCard loyaltyCard = DBHelper.getLoyaltyCard(context, database, Integer.parseInt(prevShortcut.getId()));
LoyaltyCard loyaltyCard = DBHelper.getLoyaltyCard(database, Integer.parseInt(prevShortcut.getId()));
// skip outdated cards that no longer exist
if (loyaltyCard != null) {
@@ -119,7 +120,7 @@ class ShortcutHelper {
Bitmap createAdaptiveBitmap(@NotNull Bitmap in, int paddingColor) {
Bitmap ret = Bitmap.createBitmap(ADAPTIVE_BITMAP_SIZE, ADAPTIVE_BITMAP_SIZE, Bitmap.Config.ARGB_8888);
Canvas output = new Canvas(ret);
output.drawColor(paddingColor);
output.drawColor(ColorUtils.compositeColors(PADDING_COLOR_OVERLAY, paddingColor));
Bitmap resized = Utils.resizeBitmap(in, ADAPTIVE_BITMAP_IMAGE_SIZE);
output.drawBitmap(resized, (ADAPTIVE_BITMAP_SIZE - resized.getWidth()) / 2f, (ADAPTIVE_BITMAP_SIZE - resized.getHeight()) / 2f, null);
return ret;
@@ -132,14 +133,15 @@ class ShortcutHelper {
// one replace it.
intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_SINGLE_TOP);
final Bundle bundle = new Bundle();
bundle.putInt(LoyaltyCardViewActivity.BUNDLE_ID, loyaltyCard.id);
bundle.putInt("id", loyaltyCard.id);
bundle.putBoolean("view", true);
intent.putExtras(bundle);
Bitmap iconBitmap = loyaltyCard.getImageThumbnail(context);
Bitmap iconBitmap = Utils.retrieveCardImage(context, loyaltyCard.id, ImageLocationType.icon);
if (iconBitmap == null) {
iconBitmap = Utils.generateIcon(context, loyaltyCard, true).getLetterTile();
} else {
iconBitmap = createAdaptiveBitmap(iconBitmap, Utils.needsDarkForeground(Utils.getHeaderColor(context, loyaltyCard)) ? Color.BLACK : Color.WHITE);
iconBitmap = createAdaptiveBitmap(iconBitmap, Utils.getHeaderColor(context, loyaltyCard));
}
IconCompat icon = IconCompat.createWithAdaptiveBitmap(iconBitmap);

View File

@@ -24,13 +24,6 @@ import com.yalantis.ucrop.UCropActivity;
public class UCropWrapper extends UCropActivity {
public static final String UCROP_TOOLBAR_TYPEFACE_STYLE = "ucop_toolbar_typeface_style";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Utils.applyWindowInsets(findViewById(android.R.id.content));
}
@Override
protected void onPostCreate(@Nullable Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);

View File

@@ -4,19 +4,15 @@ import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ImageDecoder;
import android.graphics.Matrix;
import android.graphics.pdf.PdfRenderer;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraManager;
import android.net.Uri;
import android.os.Build;
import android.os.ParcelFileDescriptor;
@@ -28,7 +24,6 @@ import android.util.Log;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.ImageView;
import android.widget.TextView;
@@ -40,26 +35,20 @@ import androidx.annotation.RawRes;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.graphics.ColorUtils;
import androidx.core.graphics.Insets;
import androidx.core.os.LocaleListCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import androidx.core.widget.TextViewCompat;
import androidx.exifinterface.media.ExifInterface;
import androidx.palette.graphics.Palette;
import com.google.android.material.color.DynamicColors;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.LuminanceSource;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.DecodeHintType;
import com.google.zxing.NotFoundException;
import com.google.zxing.RGBLuminanceSource;
import com.google.zxing.Result;
import com.google.zxing.common.GlobalHistogramBinarizer;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.multi.GenericMultipleBarcodeReader;
import com.google.zxing.multi.MultipleBarcodeReader;
@@ -85,9 +74,7 @@ import java.util.Calendar;
import java.util.Collections;
import java.util.Currency;
import java.util.Date;
import java.util.EnumMap;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -105,13 +92,12 @@ public class Utils {
public static final int BARCODE_SCAN = 3;
public static final int BARCODE_IMPORT_FROM_IMAGE_FILE = 4;
public static final int BARCODE_IMPORT_FROM_PDF_FILE = 5;
public static final int BARCODE_IMPORT_FROM_PKPASS_FILE = 6;
public static final int CARD_IMAGE_FROM_CAMERA_FRONT = 7;
public static final int CARD_IMAGE_FROM_CAMERA_BACK = 8;
public static final int CARD_IMAGE_FROM_CAMERA_ICON = 9;
public static final int CARD_IMAGE_FROM_FILE_FRONT = 10;
public static final int CARD_IMAGE_FROM_FILE_BACK = 11;
public static final int CARD_IMAGE_FROM_FILE_ICON = 12;
public static final int CARD_IMAGE_FROM_CAMERA_FRONT = 6;
public static final int CARD_IMAGE_FROM_CAMERA_BACK = 7;
public static final int CARD_IMAGE_FROM_CAMERA_ICON = 8;
public static final int CARD_IMAGE_FROM_FILE_FRONT = 9;
public static final int CARD_IMAGE_FROM_FILE_BACK = 10;
public static final int CARD_IMAGE_FROM_FILE_ICON = 11;
public static final String CARD_IMAGE_FILENAME_REGEX = "^(card_)(\\d+)(_(?:front|back|icon)\\.png)$";
@@ -154,7 +140,7 @@ public class Utils {
return ColorUtils.calculateLuminance(backgroundColor) > LUMINANCE_MIDPOINT;
}
static public List<ParseResult> retrieveBarcodesFromImage(Context context, Uri uri) {
static public List<BarcodeValues> retrieveBarcodesFromImage(Context context, Uri uri) {
Log.i(TAG, "Received image file with possible barcode");
if (uri == null) {
@@ -173,7 +159,7 @@ public class Utils {
return new ArrayList<>();
}
List<ParseResult> barcodesFromBitmap = getBarcodesFromBitmap(bitmap);
List<BarcodeValues> barcodesFromBitmap = getBarcodesFromBitmap(bitmap);
if (barcodesFromBitmap.isEmpty()) {
Log.i(TAG, "No barcode found in image file");
@@ -183,52 +169,7 @@ public class Utils {
return barcodesFromBitmap;
}
static public List<ParseResult> retrieveBarcodesFromPkPass(Context context, Uri uri) {
Log.i(TAG, "Received Pkpass file with possible barcode");
if (uri == null) {
Log.e(TAG, "Pkpass did not contain any data");
Toast.makeText(context, R.string.errorReadingFile, Toast.LENGTH_LONG).show();
return new ArrayList<>();
}
PkpassParser pkpassParser;
try {
pkpassParser = new PkpassParser(context, uri);
} catch (Exception e) {
Log.e(TAG, "Error reading pkpass file", e);
Toast.makeText(context, R.string.errorReadingFile, Toast.LENGTH_LONG).show();
return new ArrayList<>();
}
List<String> locales = pkpassParser.listLocales();
if (locales.isEmpty()) {
try {
return Collections.singletonList(new ParseResult(ParseResultType.FULL, pkpassParser.toLoyaltyCard(null)));
} catch (Exception e) {
Log.e(TAG, "Error calling toLoyaltyCard on pkpass file", e);
Toast.makeText(context, R.string.errorReadingFile, Toast.LENGTH_LONG).show();
return new ArrayList<>();
}
}
List<ParseResult> parseResultList = new ArrayList<>();
for (String locale : locales) {
ParseResult parseResult;
try {
parseResult = new ParseResult(ParseResultType.FULL, pkpassParser.toLoyaltyCard(locale));
} catch (Exception e) {
Log.e(TAG, "Error calling toLoyaltyCard on pkpass file", e);
Toast.makeText(context, R.string.errorReadingFile, Toast.LENGTH_LONG).show();
return new ArrayList<>();
}
parseResult.setNote(locale);
parseResultList.add(parseResult);
}
return parseResultList;
}
static public List<ParseResult> retrieveBarcodesFromPdf(Context context, Uri uri) {
static public List<BarcodeValues> retrieveBarcodesFromPdf(Context context, Uri uri) {
Log.i(TAG, "Received PDF file with possible barcode");
if (uri == null) {
Log.e(TAG, "Uri did not contain any data");
@@ -238,7 +179,7 @@ public class Utils {
ParcelFileDescriptor parcelFileDescriptor = null;
PdfRenderer renderer = null;
List<ParseResult> barcodesFromPdfPages = new ArrayList<>();
List<BarcodeValues> barcodesFromPdfPages = new ArrayList<>();
try {
parcelFileDescriptor = context.getContentResolver().openFileDescriptor(uri, "r");
@@ -250,20 +191,13 @@ public class Utils {
for (int i = 0; i < renderer.getPageCount(); i++) {
PdfRenderer.Page page = renderer.openPage(i);
renderedPage = Bitmap.createBitmap(page.getWidth(), page.getHeight(), Bitmap.Config.ARGB_8888);
// Ensure the page has a background
// Fixes some transparent PDF files not being read well
Canvas canvas = new Canvas(renderedPage);
canvas.drawColor(Color.WHITE);
canvas.drawBitmap(renderedPage, 0, 0, null);
page.render(renderedPage, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
page.close();
List<ParseResult> barcodesFromPage = getBarcodesFromBitmap(renderedPage);
for (ParseResult parseResult : barcodesFromPage) {
parseResult.setNote(String.format(context.getString(R.string.pageWithNumber), i+1));
barcodesFromPdfPages.add(parseResult);
List<BarcodeValues> barcodesFromPage = getBarcodesFromBitmap(renderedPage);
for (BarcodeValues barcodeValues : barcodesFromPage) {
barcodeValues.setNote(String.format(context.getString(R.string.pageWithNumber), i+1));
barcodesFromPdfPages.add(barcodeValues);
}
}
}
@@ -292,17 +226,17 @@ public class Utils {
}
/**
* Returns the ParseResult based on the result of an activity.
* It shows toasts to notify the end-user as needed itself and will return an empty list if the
* activity was cancelled or nothing could be found.
* Returns the Barcode format and content based on the result of an activity.
* It shows toasts to notify the end-user as needed itself and will return an empty
* BarcodeValues object if the activity was cancelled or nothing could be found.
*
* @param requestCode
* @param resultCode
* @param intent
* @param context
* @return List<ParseResult>
* @return BarcodeValues
*/
static public List<ParseResult> parseSetBarcodeActivityResult(int requestCode, int resultCode, Intent intent, Context context) {
static public List<BarcodeValues> parseSetBarcodeActivityResult(int requestCode, int resultCode, Intent intent, Context context) {
String contents;
String format;
@@ -318,10 +252,6 @@ public class Utils {
return retrieveBarcodesFromPdf(context, intent.getData());
}
if (requestCode == Utils.BARCODE_IMPORT_FROM_PKPASS_FILE) {
return retrieveBarcodesFromPkPass(context, intent.getData());
}
if (requestCode == Utils.BARCODE_SCAN || requestCode == Utils.SELECT_BARCODE_REQUEST) {
if (requestCode == Utils.BARCODE_SCAN) {
Log.i(TAG, "Received barcode information from camera");
@@ -335,15 +265,7 @@ public class Utils {
Log.i(TAG, "Read barcode id: " + contents);
Log.i(TAG, "Read format: " + format);
LoyaltyCard loyaltyCard = new LoyaltyCard();
if (format != null) {
loyaltyCard.setBarcodeType(CatimaBarcode.fromName(format));
}
if (contents != null) {
loyaltyCard.setCardId(contents);
}
return Collections.singletonList(new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard));
return Collections.singletonList(new BarcodeValues(format, contents));
}
throw new UnsupportedOperationException("Unknown request code for parseSetBarcodeActivityResult");
@@ -363,7 +285,7 @@ public class Utils {
return MediaStore.Images.Media.getBitmap(context.getContentResolver(), data);
}
static public List<ParseResult> getBarcodesFromBitmap(Bitmap bitmap) {
static public List<BarcodeValues> getBarcodesFromBitmap(Bitmap bitmap) {
// This function is vulnerable to OOM, so we try again with a smaller bitmap is we get OOM
for (int i = 0; i < 10; i++) {
try {
@@ -378,67 +300,59 @@ public class Utils {
return new ArrayList<>();
}
static private List<ParseResult> getBarcodesFromBitmapReal(Bitmap bitmap) {
static private List<BarcodeValues> getBarcodesFromBitmapReal(Bitmap bitmap) {
// In order to decode it, the Bitmap must first be converted into a pixel array...
int[] intArray = new int[bitmap.getWidth() * bitmap.getHeight()];
bitmap.getPixels(intArray, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
// ...and then turned into a binary bitmap from its luminance
LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), intArray);
BinaryBitmap binaryBitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source));
BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source));
List<ParseResult> parseResultList = new ArrayList<>();
List<BarcodeValues> barcodeValuesList = new ArrayList<>();
try {
MultiFormatReader multiFormatReader = new MultiFormatReader();
MultipleBarcodeReader multipleBarcodeReader = new GenericMultipleBarcodeReader(multiFormatReader);
Map<DecodeHintType, Object> hints = new EnumMap<>(DecodeHintType.class);
hints.put(DecodeHintType.ALSO_INVERTED, Boolean.TRUE);
Result[] barcodeResults = multipleBarcodeReader.decodeMultiple(binaryBitmap,hints);
Result[] barcodeResults = multipleBarcodeReader.decodeMultiple(binaryBitmap);
for (Result barcodeResult : barcodeResults) {
Log.i(TAG, "Read barcode id: " + barcodeResult.getText());
Log.i(TAG, "Read format: " + barcodeResult.getBarcodeFormat().name());
LoyaltyCard loyaltyCard = new LoyaltyCard();
loyaltyCard.setCardId(barcodeResult.getText());
loyaltyCard.setBarcodeType(CatimaBarcode.fromBarcode(barcodeResult.getBarcodeFormat()));
parseResultList.add(new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard));
barcodeValuesList.add(new BarcodeValues(barcodeResult.getBarcodeFormat().name(), barcodeResult.getText()));
}
return parseResultList;
return barcodeValuesList;
} catch (NotFoundException e) {
return parseResultList;
return barcodeValuesList;
}
}
static public void makeUserChooseParseResultFromList(Context context, List<ParseResult> parseResultList, ParseResultListDisambiguatorCallback callback) {
static public void makeUserChooseBarcodeFromList(Context context, List<BarcodeValues> barcodeValuesList, BarcodeValuesListDisambiguatorCallback callback) {
// If there is only one choice, consider it chosen
if (parseResultList.size() == 1) {
callback.onUserChoseParseResult(parseResultList.get(0));
if (barcodeValuesList.size() == 1) {
callback.onUserChoseBarcode(barcodeValuesList.get(0));
return;
}
// Ask user to choose a barcode
// TODO: This should contain an image of the barcode in question to help users understand the choice they're making
CharSequence[] barcodeDescriptions = new CharSequence[parseResultList.size()];
for (int i = 0; i < parseResultList.size(); i++) {
ParseResult parseResult = parseResultList.get(i);
CatimaBarcode catimaBarcode = parseResult.getLoyaltyCard().barcodeType;
CharSequence[] barcodeDescriptions = new CharSequence[barcodeValuesList.size()];
for (int i = 0; i < barcodeValuesList.size(); i++) {
BarcodeValues barcodeValues = barcodeValuesList.get(i);
CatimaBarcode catimaBarcode = CatimaBarcode.fromName(barcodeValues.format());
String barcodeContent = parseResult.getLoyaltyCard().cardId;
String barcodeContent = barcodeValues.content();
// Shorten overly long barcodes
if (barcodeContent.length() > 22) {
barcodeContent = barcodeContent.substring(0, 20) + "";
}
String parseResultNote = parseResult.getNote();
if (parseResultNote != null) {
barcodeDescriptions[i] = String.format("%s: %s (%s)", parseResultNote, catimaBarcode != null ? catimaBarcode.prettyName() : context.getString(R.string.noBarcode), barcodeContent);
if (barcodeValues.note() != null) {
barcodeDescriptions[i] = String.format("%s: %s (%s)", barcodeValues.note(), catimaBarcode.prettyName(), barcodeContent);
} else {
barcodeDescriptions[i] = String.format("%s (%s)", catimaBarcode != null ? catimaBarcode.prettyName() : context.getString(R.string.noBarcode), barcodeContent);
barcodeDescriptions[i] = String.format("%s (%s)", catimaBarcode.prettyName(), barcodeContent);
}
}
@@ -446,7 +360,7 @@ public class Utils {
builder.setTitle(context.getString(R.string.multipleBarcodesFoundPleaseChooseOne));
builder.setItems(
barcodeDescriptions,
(dialogInterface, i) -> callback.onUserChoseParseResult(parseResultList.get(i))
(dialogInterface, i) -> callback.onUserChoseBarcode(barcodeValuesList.get(i))
);
builder.setOnCancelListener(dialogInterface -> callback.onUserDismissedSelector());
builder.show();
@@ -596,11 +510,6 @@ public class Utils {
double width = bitmap.getWidth();
double height = bitmap.getHeight();
// Early exit
if (Math.max(width, height) <= maxSize) {
return bitmap;
}
if (height > width) {
double scale = height / maxSize;
height = maxSize;
@@ -871,7 +780,7 @@ public class Utils {
}
}
public static @Nullable Bitmap loadImage(String path) {
public static Bitmap loadImage(String path) {
try {
return BitmapFactory.decodeStream(new FileInputStream(path));
} catch (IOException e) {
@@ -880,7 +789,7 @@ public class Utils {
}
}
public static @Nullable Bitmap loadTempImage(Context context, String name) {
public static Bitmap loadTempImage(Context context, String name) {
return loadImage(context.getCacheDir() + "/" + name);
}
@@ -957,7 +866,7 @@ public class Utils {
return typedValue.data;
}
public static int getHeaderColorFromImage(@Nullable Bitmap image, int fallback) {
public static int getHeaderColorFromImage(Bitmap image, int fallback) {
if (image == null) {
return fallback;
}
@@ -1022,40 +931,14 @@ public class Utils {
* @param textWhenNoImage TextView to write the loyalty card name into if icon is null
* @return background colour
*/
public static int setIconOrTextWithBackground(Context context, LoyaltyCard loyaltyCard, Bitmap icon, ImageView backgroundOrIcon, TextView textWhenNoImage, int columnCount) {
public static int setIconOrTextWithBackground(Context context, LoyaltyCard loyaltyCard, Bitmap icon, ImageView backgroundOrIcon, TextView textWhenNoImage) {
int headerColor = getHeaderColor(context, loyaltyCard);
backgroundOrIcon.setImageBitmap(icon);
backgroundOrIcon.setBackgroundColor(headerColor);
if (icon != null) {
// Use header colour to decide if this image will need a white or black background
backgroundOrIcon.setBackgroundColor(needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
// Ensure correct cropping style
backgroundOrIcon.setScaleType(Utils.getRecommendedScaleTypeForThumbnailImage(icon));
textWhenNoImage.setVisibility(View.GONE);
} else {
// Use header colour as background colour
backgroundOrIcon.setBackgroundColor(headerColor);
// Manually calculate how many lines will be needed
// This is necessary because Android's auto sizing will split over lines way before reaching the minimum font size and store names split over multiple lines are harder to scan with a quick glance so we should try to prevent it
// Because we have to write the text before we can actually know the exact laid out size (trying to delay this causes bugs where the autosize fails) we have to take some... weird shortcuts
// At this point textWhenNoImage.getWidth() still returns 0, so we cheat by calculating the whole width of the screen and then dividing it by the amount of columns
int columnWidth = Resources.getSystem().getDisplayMetrics().widthPixels / columnCount;
// Calculate how wide a character is and calculate how many characters fit in a line
// text size is generally based on height, so setting 1:1 as width may be fishy
int characterWidth = TextViewCompat.getAutoSizeMinTextSize(textWhenNoImage);
int maxWidthPerLine = columnWidth - textWhenNoImage.getPaddingStart() - textWhenNoImage.getPaddingEnd();
// Set number of lines based on what could fit at most
int fullTextWidth = loyaltyCard.store.length() * characterWidth;
int maxLines = (fullTextWidth / maxWidthPerLine) + 1;
textWhenNoImage.setMaxLines(maxLines);
// Actually set the text and colour
textWhenNoImage.setVisibility(View.VISIBLE);
textWhenNoImage.setText(loyaltyCard.store);
textWhenNoImage.setTextColor(Utils.needsDarkForeground(headerColor) ? Color.BLACK : Color.WHITE);
@@ -1064,6 +947,21 @@ public class Utils {
return headerColor;
}
public static boolean installedFromGooglePlay(Context context) {
try {
String packageName = context.getPackageName();
String installer;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
installer = context.getPackageManager().getInstallSourceInfo(packageName).getInstallingPackageName();
} else {
installer = context.getPackageManager().getInstallerPackageName(packageName);
}
return installer.equals("com.android.vending");
} catch (Throwable ignored) {
return false;
}
}
public static int getHeaderColor(Context context, LoyaltyCard loyaltyCard) {
return loyaltyCard.headerColor != null ? loyaltyCard.headerColor : LetterBitmap.getDefaultColor(context, loyaltyCard.store);
}
@@ -1117,100 +1015,4 @@ public class Utils {
return false;
});
}
public static boolean deviceHasCamera(Context context) {
try {
return ((CameraManager) context.getSystemService(Context.CAMERA_SERVICE)).getCameraIdList().length > 0;
} catch (CameraAccessException e) {
return false;
}
}
public static void applyWindowInsets(View root) {
/* This function basically fakes the activity being edge-to-edge. Useful for those activities that are really hard to get to behave well */
ViewCompat.setOnApplyWindowInsetsListener(root, (view, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
layoutParams.leftMargin = insets.left;
layoutParams.bottomMargin = insets.bottom;
layoutParams.rightMargin = insets.right;
layoutParams.topMargin = insets.top;
view.setLayoutParams(layoutParams);
return WindowInsetsCompat.CONSUMED;
});
}
public static void applyWindowInsetsAndFabOffset(View root, FloatingActionButton fab) {
/* This function is a copy of applyWindowInsets, with the added behaviour that it ensures the FAB will be displayed vertically above the keyboard at all times */
ViewCompat.setOnApplyWindowInsetsListener(root, (view, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
layoutParams.leftMargin = insets.left;
layoutParams.bottomMargin = insets.bottom;
layoutParams.rightMargin = insets.right;
layoutParams.topMargin = insets.top;
view.setLayoutParams(layoutParams);
// This is required to move the FAB above the keyboard when keyboard is open
Insets imeInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime());
boolean isKeyboardVisible = windowInsets.isVisible(WindowInsetsCompat.Type.ime());
fab.setTranslationY(isKeyboardVisible ? (- imeInsets.bottom) : 0);
return WindowInsetsCompat.CONSUMED;
});
}
public static ImageView.ScaleType getRecommendedScaleTypeForThumbnailImage(@Nullable Bitmap image) {
// Return something sensible if no image
if (image == null) {
return ImageView.ScaleType.FIT_CENTER;
}
// If the image is relatively close to 85.6:53.98 (width = 1.58577250834 * height), allow cropping it to fit it
double ratio = (double) image.getWidth() / image.getHeight();
if (ratio >= 1.55 && ratio <= 1.60) {
return ImageView.ScaleType.CENTER_CROP;
}
return ImageView.ScaleType.FIT_CENTER;
}
public static DBHelper.LoyaltyCardOrder getLoyaltyCardOrder(Context context) {
SharedPreferences sortPref = context.getSharedPreferences(
"sharedpreference_sort",
Context.MODE_PRIVATE
);
String orderString = sortPref.getString("sharedpreference_sort_order", null);
if (orderString != null) {
try {
return DBHelper.LoyaltyCardOrder.valueOf(orderString);
} catch (IllegalArgumentException ignored) {
}
}
return DBHelper.LoyaltyCardOrder.Alpha;
}
public static DBHelper.LoyaltyCardOrderDirection getLoyaltyCardOrderDirection(Context context) {
SharedPreferences sortPref = context.getSharedPreferences(
"sharedpreference_sort",
Context.MODE_PRIVATE
);
String orderDirectionString = sortPref.getString("sharedpreference_sort_direction", null);
if (orderDirectionString != null) {
try {
return DBHelper.LoyaltyCardOrderDirection.valueOf(orderDirectionString);
} catch (IllegalArgumentException ignored) {
}
}
return DBHelper.LoyaltyCardOrderDirection.Ascending;
}
}

View File

@@ -23,7 +23,7 @@ public class ZipUtils {
return new JSONObject(read(zipInputStream));
}
public static String read(ZipInputStream zipInputStream) throws IOException {
private static String read(ZipInputStream zipInputStream) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
Reader reader = new BufferedReader(new InputStreamReader(zipInputStream, StandardCharsets.UTF_8));
int c;

View File

@@ -49,7 +49,7 @@ public class CatimaExporter implements Exporter {
// Generate CSV
ByteArrayOutputStream catimaOutputStream = new ByteArrayOutputStream();
OutputStreamWriter catimaOutputStreamWriter = new OutputStreamWriter(catimaOutputStream, StandardCharsets.UTF_8);
writeCSV(context, database, catimaOutputStreamWriter);
writeCSV(database, catimaOutputStreamWriter);
// Add CSV to zip file
ZipParameters csvZipParameters = createZipParameters("catima.csv", password);
@@ -64,12 +64,12 @@ public class CatimaExporter implements Exporter {
Cursor cardCursor = DBHelper.getLoyaltyCardCursor(database);
while (cardCursor.moveToNext()) {
// For each card
LoyaltyCard card = LoyaltyCard.fromCursor(context, cardCursor);
LoyaltyCard card = LoyaltyCard.toLoyaltyCard(cardCursor);
// For each image
for (ImageLocationType imageLocationType : ImageLocationType.values()) {
// If it exists, add to the .zip file
Bitmap image = card.getImageForImageLocationType(context, imageLocationType);
Bitmap image = Utils.retrieveCardImage(context, card.id, imageLocationType);
if (image != null) {
ZipParameters imageZipParameters = createZipParameters(Utils.getCardImageFileName(card.id, imageLocationType), password);
zipOutputStream.putNextEntry(imageZipParameters);
@@ -95,7 +95,7 @@ public class CatimaExporter implements Exporter {
return zipParameters;
}
private void writeCSV(Context context, SQLiteDatabase database, OutputStreamWriter output) throws IOException, InterruptedException {
private void writeCSV(SQLiteDatabase database, OutputStreamWriter output) throws IOException, InterruptedException {
CSVPrinter printer = new CSVPrinter(output, CSVFormat.RFC4180);
// Print the version
@@ -142,7 +142,7 @@ public class CatimaExporter implements Exporter {
Cursor cardCursor = DBHelper.getLoyaltyCardCursor(database);
while (cardCursor.moveToNext()) {
LoyaltyCard card = LoyaltyCard.fromCursor(context, cardCursor);
LoyaltyCard card = LoyaltyCard.toLoyaltyCard(cardCursor);
printer.printRecord(card.id,
card.store,
@@ -176,7 +176,7 @@ public class CatimaExporter implements Exporter {
Cursor cardCursor2 = DBHelper.getLoyaltyCardCursor(database);
while (cardCursor2.moveToNext()) {
LoyaltyCard card = LoyaltyCard.fromCursor(context, cardCursor2);
LoyaltyCard card = LoyaltyCard.toLoyaltyCard(cardCursor2);
for (Group group : DBHelper.getLoyaltyCardGroups(database, card.id)) {
printer.printRecord(card.id, group._id);

View File

@@ -124,7 +124,7 @@ public class CatimaImporter implements Importer {
Set<String> existingImages = DBHelper.imageFiles(context, database);
for (LoyaltyCard card : data.cards) {
LoyaltyCard existing = DBHelper.getLoyaltyCard(context, database, card.id);
LoyaltyCard existing = DBHelper.getLoyaltyCard(database, card.id);
if (existing == null) {
DBHelper.insertLoyaltyCard(database, card.id, card.store, card.note, card.validFrom, card.expiry, card.balance, card.balanceType,
card.cardId, card.barcodeId, card.barcodeType, card.headerColor, card.starStatus, card.lastUsed, card.archiveStatus);
@@ -152,7 +152,7 @@ public class CatimaImporter implements Importer {
}
public boolean isDuplicate(Context context, final LoyaltyCard existing, final LoyaltyCard card, final Set<String> existingImages, final Map<String, String> imageChecksums) throws IOException {
if (!LoyaltyCard.isDuplicate(context, existing, card)) {
if (!LoyaltyCard.isDuplicate(existing, card)) {
return false;
}
for (ImageLocationType imageLocationType : ImageLocationType.values()) {
@@ -490,30 +490,7 @@ public class CatimaImporter implements Importer {
// We catch this exception so we can still import old backups
}
return new LoyaltyCard(
id,
store,
note,
validFrom,
expiry,
balance,
balanceType,
cardId,
barcodeId,
barcodeType,
headerColor,
starStatus,
lastUsed,
DBHelper.DEFAULT_ZOOM_LEVEL,
DBHelper.DEFAULT_ZOOM_LEVEL_WIDTH,
archiveStatus,
null,
null,
null,
null,
null,
null
);
return new LoyaltyCard(id, store, note, validFrom, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headerColor, starStatus, lastUsed, DBHelper.DEFAULT_ZOOM_LEVEL, archiveStatus);
}
/**

View File

@@ -149,30 +149,7 @@ public class FidmeImporter implements Importer {
// TODO: Front and back image
// use -1 for the ID, it will be ignored when inserting the card into the DB
return new LoyaltyCard(
-1,
store,
note,
null,
null,
BigDecimal.valueOf(0),
null,
cardId,
null,
barcodeType,
headerColor,
starStatus,
Utils.getUnixTime(),
DBHelper.DEFAULT_ZOOM_LEVEL,
DBHelper.DEFAULT_ZOOM_LEVEL_WIDTH,
archiveStatus,
null,
null,
null,
null,
null,
null
);
return new LoyaltyCard(-1, store, note, null, null, BigDecimal.valueOf(0), null, cardId, null, barcodeType, headerColor, starStatus, Utils.getUnixTime(), DBHelper.DEFAULT_ZOOM_LEVEL, archiveStatus);
}
public void saveAndDeduplicate(SQLiteDatabase database, final ImportedData data) {

View File

@@ -0,0 +1,212 @@
package protect.card_locker.importexport;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.work.Data;
import androidx.work.ForegroundInfo;
import androidx.work.WorkManager;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import protect.card_locker.DBHelper;
import protect.card_locker.NotificationHelper;
import protect.card_locker.R;
public class ImportExportWorker extends Worker {
private final String TAG = "Catima";
public static final String INPUT_URI = "uri";
public static final String INPUT_ACTION = "action";
public static final String INPUT_FORMAT = "format";
public static final String INPUT_PASSWORD = "password";
public static final String ACTION_IMPORT = "import";
public static final String ACTION_EXPORT = "export";
public static final String OUTPUT_ERROR_REASON = "errorReason";
public static final String ERROR_GENERIC = "errorTypeGeneric";
public static final String ERROR_PASSWORD_REQUIRED = "errorTypePasswordRequired";
public static final String OUTPUT_ERROR_DETAILS = "errorDetails";
public ImportExportWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
@NonNull
@Override
public Result doWork() {
Log.e("CATIMA", "Started import/export worker");
Context context = getApplicationContext();
Data inputData = getInputData();
String uriString = inputData.getString(INPUT_URI);
String action = inputData.getString(INPUT_ACTION);
String format = inputData.getString(INPUT_FORMAT);
String password = inputData.getString(INPUT_PASSWORD);
if (action.equals(ACTION_IMPORT)) {
Log.e("CATIMA", "Import requested");
setForegroundAsync(createForegroundInfo(NotificationHelper.CHANNEL_IMPORT, NotificationHelper.IMPORT_PROGRESS_ID, R.string.importing));
ImportExportResult result;
InputStream stream;
try {
stream = context.getContentResolver().openInputStream(Uri.parse(uriString));
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
final SQLiteDatabase database = new DBHelper(context).getWritableDatabase();
try {
InputStreamReader writer = new InputStreamReader(stream, StandardCharsets.UTF_8);
result = MultiFormatImporter.importData(context, database, stream, DataFormat.valueOf(format), password.toCharArray());
writer.close();
} catch (IOException e) {
Log.e(TAG, "Unable to import file", e);
NotificationHelper.sendNotification(context, NotificationHelper.IMPORT_ID, NotificationHelper.createNotificationBuilder(context, NotificationHelper.CHANNEL_IMPORT, R.drawable.ic_import_export_white_24dp, context.getString(R.string.importFailedTitle), e.getLocalizedMessage()).build());
Data failureData = new Data.Builder()
.putString(OUTPUT_ERROR_REASON, ERROR_GENERIC)
.putString(OUTPUT_ERROR_DETAILS, e.getLocalizedMessage())
.putString(INPUT_URI, uriString)
.putString(INPUT_ACTION, action)
.putString(INPUT_FORMAT, format)
.putString(INPUT_PASSWORD, password)
.build();
return Result.failure(failureData);
}
Log.i(TAG, "Import result: " + result);
if (result.resultType() == ImportExportResultType.Success) {
NotificationHelper.sendNotification(context, NotificationHelper.IMPORT_ID, NotificationHelper.createNotificationBuilder(context, NotificationHelper.CHANNEL_IMPORT, R.drawable.ic_import_export_white_24dp, context.getString(R.string.importSuccessfulTitle), context.getString(R.string.importSuccessful)).build());
return Result.success();
} else if (result.resultType() == ImportExportResultType.BadPassword) {
Log.e(TAG, "Needs password, unhandled for now");
NotificationHelper.sendNotification(context, NotificationHelper.IMPORT_ID, NotificationHelper.createNotificationBuilder(context, NotificationHelper.CHANNEL_IMPORT, R.drawable.ic_import_export_white_24dp, context.getString(R.string.importing), context.getString(R.string.passwordRequired)).build());
Data failureData = new Data.Builder()
.putString(OUTPUT_ERROR_REASON, ERROR_PASSWORD_REQUIRED)
.putString(OUTPUT_ERROR_DETAILS, result.developerDetails())
.putString(INPUT_URI, uriString)
.putString(INPUT_ACTION, action)
.putString(INPUT_FORMAT, format)
.putString(INPUT_PASSWORD, password)
.build();
return Result.failure(failureData);
} else {
NotificationHelper.sendNotification(context, NotificationHelper.IMPORT_ID, NotificationHelper.createNotificationBuilder(context, NotificationHelper.CHANNEL_IMPORT, R.drawable.ic_import_export_white_24dp, context.getString(R.string.importFailedTitle), context.getString(R.string.importFailed)).build());
Data failureData = new Data.Builder()
.putString(OUTPUT_ERROR_REASON, ERROR_GENERIC)
.putString(OUTPUT_ERROR_DETAILS, result.developerDetails())
.putString(INPUT_URI, uriString)
.putString(INPUT_ACTION, action)
.putString(INPUT_FORMAT, format)
.putString(INPUT_PASSWORD, password)
.build();
return Result.failure(failureData);
}
} else {
Log.e("CATIMA", "Export requested");
setForegroundAsync(createForegroundInfo(NotificationHelper.CHANNEL_EXPORT, NotificationHelper.EXPORT_PROGRESS_ID, R.string.exporting));
ImportExportResult result;
OutputStream stream;
try {
stream = context.getContentResolver().openOutputStream(Uri.parse(uriString));
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
final SQLiteDatabase database = new DBHelper(context).getReadableDatabase();
try {
OutputStreamWriter writer = new OutputStreamWriter(stream, StandardCharsets.UTF_8);
result = MultiFormatExporter.exportData(context, database, stream, DataFormat.valueOf(format), password.toCharArray());
writer.close();
} catch (IOException e) {
Log.e(TAG, "Unable to export file", e);
NotificationHelper.sendNotification(context, NotificationHelper.EXPORT_ID, NotificationHelper.createNotificationBuilder(context, NotificationHelper.CHANNEL_EXPORT, R.drawable.ic_import_export_white_24dp, context.getString(R.string.exportFailedTitle), e.getLocalizedMessage()).build());
Data failureData = new Data.Builder()
.putString(OUTPUT_ERROR_REASON, ERROR_GENERIC)
.putString(OUTPUT_ERROR_DETAILS, e.getLocalizedMessage())
.putString(INPUT_URI, uriString)
.putString(INPUT_ACTION, action)
.putString(INPUT_FORMAT, format)
.putString(INPUT_PASSWORD, password)
.build();
return Result.failure(failureData);
}
Log.i(TAG, "Export result: " + result);
if (result.resultType() == ImportExportResultType.Success) {
NotificationHelper.sendNotification(context, NotificationHelper.EXPORT_ID, NotificationHelper.createNotificationBuilder(context, NotificationHelper.CHANNEL_EXPORT, R.drawable.ic_import_export_white_24dp, context.getString(R.string.exportSuccessfulTitle), context.getString(R.string.exportSuccessful)).build());
return Result.success();
} else {
NotificationHelper.sendNotification(context, NotificationHelper.EXPORT_ID, NotificationHelper.createNotificationBuilder(context, NotificationHelper.CHANNEL_EXPORT, R.drawable.ic_import_export_white_24dp, context.getString(R.string.exportFailedTitle), context.getString(R.string.exportFailed)).build());
Data failureData = new Data.Builder()
.putString(OUTPUT_ERROR_REASON, ERROR_GENERIC)
.putString(OUTPUT_ERROR_DETAILS, result.developerDetails())
.putString(INPUT_URI, uriString)
.putString(INPUT_ACTION, action)
.putString(INPUT_FORMAT, format)
.putString(INPUT_PASSWORD, password)
.build();
return Result.failure(failureData);
}
}
}
@NonNull
private ForegroundInfo createForegroundInfo(@NonNull String channel, int notificationId, int title) {
Context context = getApplicationContext();
String cancel = context.getString(R.string.cancel);
// This PendingIntent can be used to cancel the worker
PendingIntent intent = WorkManager.getInstance(context)
.createCancelPendingIntent(getId());
Notification.Builder notificationBuilder = NotificationHelper.createNotificationBuilder(context, channel, R.drawable.ic_import_export_white_24dp, context.getString(title), null);
Notification notification = notificationBuilder
.setOngoing(true)
// Add the cancel action to the notification which can
// be used to cancel the worker
.addAction(android.R.drawable.ic_delete, cancel, intent)
.build();
return new ForegroundInfo(notificationId, notification);
}
}

View File

@@ -45,7 +45,7 @@ public class MultiFormatImporter {
break;
}
String error = null;
String error;
if (importer != null) {
File inputFile;
try {

View File

@@ -9,9 +9,8 @@ import androidx.annotation.NonNull;
import com.google.zxing.BarcodeFormat;
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.io.inputstream.ZipInputStream;
import net.lingala.zip4j.model.FileHeader;
import net.lingala.zip4j.model.LocalFileHeader;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
@@ -21,7 +20,9 @@ import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
@@ -129,9 +130,11 @@ public class StocardImporter implements Importer {
throw new FormatException("Issue parsing CSV data", e);
}
ZipFile zipFile = new ZipFile(inputFile, password);
zipData = importZIP(zipFile, zipData);
zipFile.close();
InputStream input = new FileInputStream(inputFile);
ZipInputStream zipInputStream = new ZipInputStream(input, password);
zipData = importZIP(zipInputStream, zipData);
zipInputStream.close();
input.close();
if (zipData.cards.keySet().size() == 0) {
throw new FormatException("Couldn't find any loyalty cards in this Stocard export.");
@@ -141,7 +144,7 @@ public class StocardImporter implements Importer {
saveAndDeduplicate(context, database, importedData);
}
public ZIPData importZIP(ZipFile zipFile, final ZIPData zipData) throws IOException, FormatException, JSONException {
public ZIPData importZIP(ZipInputStream zipInputStream, final ZIPData zipData) throws IOException, FormatException, JSONException {
Map<String, StocardRecord> cards = zipData.cards;
Map<String, StocardProvider> providers = zipData.providers;
@@ -149,8 +152,9 @@ public class StocardImporter implements Importer {
String[] cardBaseName = null;
String customProviderId = "";
String cardName = "";
for (FileHeader fileHeader : zipFile.getFileHeaders()) {
String fileName = fileHeader.getFileName();
LocalFileHeader localFileHeader;
while ((localFileHeader = zipInputStream.getNextEntry()) != null) {
String fileName = localFileHeader.getFileName();
String[] nameParts = fileName.split("/");
if (nameParts.length < 2) {
@@ -158,7 +162,6 @@ public class StocardImporter implements Importer {
}
String userId = nameParts[1];
ZipInputStream zipInputStream = zipFile.getInputStream(fileHeader);
if (customProvidersBaseName == null) {
// FIXME: can we use the points-account/statement/content.json balance info somehow?
@@ -299,8 +302,6 @@ public class StocardImporter implements Importer {
} else if (!fileName.endsWith("/")) {
Log.d(TAG, "Unknown or unused file " + fileName + ", skipping...");
}
zipInputStream.close();
}
return new ZIPData(cards, providers);
@@ -354,30 +355,7 @@ public class StocardImporter implements Importer {
long lastUsed = record.lastUsed != null ? record.lastUsed : Utils.getUnixTime();
LoyaltyCard card = new LoyaltyCard(
tempID,
store,
note,
null,
null,
BigDecimal.valueOf(0),
null,
record.cardId,
null,
barcodeType,
headerColor,
0,
lastUsed,
DBHelper.DEFAULT_ZOOM_LEVEL,
DBHelper.DEFAULT_ZOOM_LEVEL_WIDTH,
0,
null,
null,
null,
null,
null,
null
);
LoyaltyCard card = new LoyaltyCard(tempID, store, note, null, null, BigDecimal.valueOf(0), null, record.cardId, null, barcodeType, headerColor, 0, lastUsed, DBHelper.DEFAULT_ZOOM_LEVEL, 0);
importedData.cards.add(card);
Map<ImageLocationType, Bitmap> images = new HashMap<>();

View File

@@ -151,30 +151,7 @@ public class VoucherVaultImporter implements Importer {
}
// use -1 for the ID, it will be ignored when inserting the card into the DB
importedData.cards.add(new LoyaltyCard(
-1,
store,
"",
null,
expiry,
balance,
balanceType,
cardId,
null,
barcodeType,
headerColor,
0,
Utils.getUnixTime(),
DBHelper.DEFAULT_ZOOM_LEVEL,
DBHelper.DEFAULT_ZOOM_LEVEL_WIDTH,
0,
null,
null,
null,
null,
null,
null
));
importedData.cards.add(new LoyaltyCard(-1, store, "", null, expiry, balance, balanceType, cardId, null, barcodeType, headerColor, 0, Utils.getUnixTime(), DBHelper.DEFAULT_ZOOM_LEVEL, 0));
}
return importedData;

View File

@@ -1,13 +1,9 @@
package protect.card_locker.preferences;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import androidx.annotation.IntegerRes;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.preference.PreferenceManager;
@@ -18,9 +14,8 @@ import protect.card_locker.R;
import protect.card_locker.Utils;
public class Settings {
private static final String TAG = "Catima";
private final Context mContext;
private final SharedPreferences mSettings;
private SharedPreferences mSettings;
public Settings(Context context) {
mContext = context.getApplicationContext();
@@ -47,11 +42,10 @@ public class Settings {
return mSettings.getBoolean(getResString(keyId), defaultValue);
}
@Nullable
public Locale getLocale() {
String value = getString(R.string.settings_key_locale, "");
if (value.isEmpty()) {
if (value.length() == 0) {
return null;
}
@@ -97,23 +91,4 @@ public class Settings {
public String getColor() {
return getString(R.string.setting_key_theme_color, mContext.getResources().getString(R.string.settings_key_system_theme));
}
public int getPreferredColumnCount() {
var defaultSymbol = mContext.getResources().getString(R.string.settings_key_automatic_column_count);
var defaultColumnCount = mContext.getResources().getInteger(R.integer.main_view_card_columns);
var orientation = mContext.getResources().getConfiguration().orientation;
var columnCountPrefKey = orientation == ORIENTATION_PORTRAIT ? R.string.setting_key_column_count_portrait : R.string.setting_key_column_count_landscape;
var columnCountSetting = getString(columnCountPrefKey, defaultSymbol);
try {
// the pref may be unset or explicitly set to default
return columnCountSetting.equals(defaultSymbol) ? defaultColumnCount : Integer.parseInt(columnCountSetting);
} catch (NumberFormatException nfe) {
Log.e(TAG, "Failed to parseInt the column count pref", nfe);
return defaultColumnCount;
}
}
public boolean useVolumeKeysForNavigation() {
return getBoolean(R.string.settings_key_use_volume_keys_navigation, false);
}
}

View File

@@ -42,7 +42,6 @@ public class SettingsActivity extends CatimaAppCompatActivity {
binding = SettingsActivityBinding.inflate(getLayoutInflater());
setTitle(R.string.settings);
setContentView(binding.getRoot());
Utils.applyWindowInsets(binding.getRoot());
Toolbar toolbar = binding.toolbar;
setSupportActionBar(toolbar);
enableToolbarBackButton();

View File

@@ -1,27 +0,0 @@
package protect.card_locker.viewmodels
import android.net.Uri
import androidx.lifecycle.ViewModel
import protect.card_locker.LoyaltyCard
import protect.card_locker.LoyaltyCardField
import protect.card_locker.async.TaskHandler
class LoyaltyCardEditActivityViewModel : ViewModel() {
var initialized: Boolean = false
var hasChanged: Boolean = false
var taskHandler: TaskHandler = TaskHandler();
var addGroup: String? = null
var openSetIconMenu: Boolean = false
var loyaltyCardId: Int = 0
var updateLoyaltyCard: Boolean = false
var duplicateFromLoyaltyCardId: Boolean = false
var importLoyaltyCardUri: Uri? = null
var tabIndex: Int = 0
var requestedImageType: Int = 0
var tempLoyaltyCardField: LoyaltyCardField? = null
var loyaltyCard: LoyaltyCard = LoyaltyCard()
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM4,12c0,-4.42 3.58,-8 8,-8 1.85,0 3.55,0.63 4.9,1.69L5.69,16.9C4.63,15.55 4,13.85 4,12zM12,20c-1.85,0 -3.55,-0.63 -4.9,-1.69L18.31,7.1C19.37,8.45 20,10.15 20,12c0,4.42 -3.58,8 -8,8z"/>
</vector>

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