Compare commits

...
132 Commits
Author SHA1 Message Date
Markus Fisch 386f882944 Advance version number to 1.73.1 2026-05-27 21:28:49 +02:00
Markus Fisch 65979cbddd Fix stale history row highlighting
Remove ListView choice mode so regular history row taps
no longer persist as checked state after returning from
the scan detail screen.
2026-05-27 21:23:44 +02:00
Markus Fisch 26747d709f Update vdstools library 2026-05-27 21:20:19 +02:00
Markus Fisch 22b6a9f66e Add a note to not update the material library yet
Because the latest version would require minSDK 23, which
is silly, but the usual nonsense from Google, unfortunately.
2026-05-27 21:19:00 +02:00
Markus Fisch ded532c523 Update ZXing-C++ and support Telepen format 2026-05-27 21:17:48 +02:00
Markus Fisch 17c49a4a27 Advance version number to 1.73.0 2026-05-26 23:06:19 +02:00
Markus Fisch 7ff8db0880 Fix deeplink argument parsing
Merge intent extras with parsed deeplink query parameters
in EncodeActivity so app links populate content and format
even when Android supplies an empty extras bundle.

This fixes opened /encode links showing an empty content
field.
2026-05-26 22:48:10 +02:00
Markus Fisch d7837f5795 Migrate to Material You and support dynamic colors
And add a preference toggle to use the original colors
if a user likes that better.
2026-05-26 22:28:13 +02:00
Markus Fisch 1f5c77e802 Add preference to toggle tracking param stripping
Make the URL tracking parameter sanitization introduced opt-out
rather than always-on.
2026-05-26 21:35:58 +02:00
Markus Fisch a4da488a41 Refactor "execShareIntent" to "startIntentOrToast"
A better description of what it does.
2026-05-26 21:12:09 +02:00
Markus Fisch d177c08420 Refactor "fired" to "executed"
Which is a better fit, because we already have an "execute"
function.
2026-05-26 21:07:52 +02:00
Markus Fisch a6123ed233 Optimize imports
With Android Studio.
2026-05-26 21:07:17 +02:00
Markus Fisch 2ebb376a8b Remove redundant let scope function 2026-05-26 21:06:45 +02:00
Markus Fisch 6cac7e8ead Simplify sanitizing URIs
Make stripTrackingParams a top-level extension function,
which is a bit simpler and cleaner in my opinion.
2026-05-26 21:04:42 +02:00
Ashutosh TiwariandGitHub 59f6c9f654 Strip common tracking parameters before opening scanned URLs
Many QR codes in the wild contain analytics tracking parameters
(utm_*, fbclid, gclid, mc_*, _ga, igshid, …) that uniquely identify
the scanning user/session. These add nothing for the user and are
silently propagated to the destination site when openUri() launches
ACTION_VIEW.

Add a tiny content/UrlSanitizer helper that filters a curated set of
~30 well-known tracker parameter names from http(s) URIs, and call it
from openUri() so cleanup happens transparently on every URL launch.

The list is intentionally conservative: only parameters whose sole
purpose is third-party attribution. Functional/site parameters
(q, id, v, page, search, …) are left untouched. Non-http(s) URIs and
URIs without a query string are returned as-is, so there's no
behavioral change for tel:, mailto:, geo:, content:, etc.
2026-05-26 18:19:35 +02:00
Markus Fisch f0c1d2a936 Add a note for cameraX and core-ktx
To stay on these versions because the next ones won't
support minSDK 21 anymore.

Idiots at keyboards  ̄\_(ツ)_/ ̄
2026-05-11 21:54:35 +02:00
Markus Fisch e7aff150f6 Update bouncycastle library 2026-05-11 21:53:11 +02:00
Markus Fisch 10755555ff Update vdstools library 2026-05-11 21:53:01 +02:00
Markus Fisch a78a85df2c Update kotlinx-datetime library 2026-05-11 21:51:46 +02:00
Markus Fisch de9c490429 Update kotlin coroutines library 2026-05-11 21:51:17 +02:00
Markus Fisch 77e686af75 Update tools version 2026-05-11 21:46:14 +02:00
Markus Fisch 0ae13f22b6 Complete missing Latvian translations 2026-05-11 21:43:34 +02:00
Too Late (bot)andGitHub bedfcdec14 Translations update from Toolate 2026-05-11 21:37:34 +02:00
bovirusandGitHub a6ba968ea8 Italian language update 2026-05-06 17:21:28 +02:00
Markus Fisch 5301da5900 Add a blank line between parsers
Easier to read.
2026-05-05 17:03:42 +02:00
Markus Fisch cb7d64991b Parse and display otpauth:// code fields
Show type, account, issuer, algorithm, digits, and
period/counter when scanning an OTP code.
2026-05-05 17:03:42 +02:00
Markus Fisch 6194dc29d5 Update tools version 2026-05-05 17:03:42 +02:00
bovirusandGitHub 3fa39b9fe0 Update Italian language 2026-04-24 21:54:41 +02:00
Markus Fisch f37b846241 Advance version number to 1.72.2 2026-04-23 23:40:35 +02:00
Markus Fisch 8ce33c0071 Add predefined expert and simple profiles
So users can always switch between these profiles.
2026-04-23 18:28:51 +02:00
Markus Fisch e6823386b0 Move setCovered to Mapping.kt
It belongs alongside the other frame/view coordinate
helpers (setFrameRoi, setFrameToView).
2026-04-22 23:20:26 +02:00
Markus Fisch 3e57a59e16 Avoid shared camera mapping state
Move ROI and frame mapping data out of shared mutable activity
fields.

Capture view geometry on the UI thread, then build per-frame
mapping data inside the analyzer so decoding, crosshair checks,
and overlay updates use a consistent snapshot.
2026-04-22 22:58:07 +02:00
Markus Fisch 758220200e Fix isSilent() for media stream
Ringer mode is irrelevant for STREAM_MUSIC. Audio plays in
vibrate mode regardless. Check stream volume instead, so beeps
are only suppressed when the media volume is actually zero.
2026-04-22 22:42:59 +02:00
Markus Fisch b3584392b3 Set share QR listener once per ViewHolder
Not per bind. Allocating a new lambda on every getView()
call is wasteful.
2026-04-22 22:12:36 +02:00
Markus Fisch 3bb26f06d1 Import/export preferences using QR codes
Any profile can be exported/imported via a special QR code now.
2026-04-23 16:11:31 +02:00
Markus Fisch fe4a021cd0 Fix CameraX preview teardown leak 2026-04-22 22:02:59 +02:00
Markus Fisch 0de5565b78 Update tools version 2026-04-20 22:04:04 +02:00
Markus Fisch d9a9acd8c1 Update ZXingCpp library to v3.0.2.5 2026-04-21 13:01:01 +02:00
Markus Fisch 1be31f71ed Update ZXingCpp library to v3.0.2.4 2026-04-21 13:00:58 +02:00
Markus Fisch 8859c61119 Advance version number to 1.72.1 2026-04-11 13:06:04 +02:00
Andrew GunnersonandGitHub c52778f59d Fix crash on upgrades due to missing DATA_BAR entry
Commit 73047cdee6 merged the old
migrateToNativeFormatNames() and migrateBarcodeFormatName() mapping
functions, but the new map was based on the former, which happened to be
missing the entry for DATA_BAR.

Fixes: #636

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-04-11 12:53:08 +02:00
bovirusandGitHub 5bde3947e3 Italian language update 2026-04-10 22:19:16 +02:00
Markus Fisch 08be31814f Advance version number to 1.72.0 2026-04-09 23:05:29 +02:00
Markus Fisch c5db04b84e Add headlines for result sections
Makes result sections easier to digest.
2026-04-09 21:16:27 +02:00
Markus Fisch 9dccc6a7f5 Fix EPC QR parser tests 2026-04-09 16:46:20 +02:00
Markus Fisch a5deab656b Clamp invalid ROIs to prevent crashes
Fixes a crash path where rounding in the view-to-frame
mapping could produce crop rectangles outside the Y plane.
2026-04-09 16:46:20 +02:00
Markus Fisch b773dfa392 Remove double signature date item for IDB codes 2026-04-09 16:46:20 +02:00
Markus Fisch 7b5106f09d Parse JSON content and show pretty-printed dump 2026-04-09 16:46:16 +02:00
Markus Fisch 16ce45093e Use list selector for parsed data items
So there is some touch feedback.
2026-04-09 19:47:44 +02:00
Markus Fisch e6345f09b3 Reformat code 2026-04-09 12:59:04 +02:00
Markus Fisch de00ed779a Update vdstools to latest version
And migrate the code to use the public API only.

This version doesn't include the Kermit logging library
which would have required a minSdk raise.
2026-04-09 10:58:34 +02:00
Markus Fisch d740aa121b Advance version number to 1.71.0 2026-04-08 21:56:07 +02:00
Markus Fisch 38c40d36bb Preserve parsed data while editing
Keep the last successfully parsed data rows visible in the decode
view while the barcode content is being edited. Avoids jumping of
the layout.
2026-04-08 19:49:25 +02:00
Markus Fisch 3e51f70c7c Decode nested IDB messages
Flatten nested IDB messages into seal fields and decode child values
using the schema-defined coding from vdstools. This fixes nested
content such as MRZ fields inside IDB container messages being shown
as raw byte counts instead of decoded text.
2026-04-08 19:11:26 +02:00
Markus Fisch 502702c7a5 Remove unused indent fields 2026-04-08 19:10:03 +02:00
Markus Fisch 96ff9b6239 Change data table to a label/value list
So values have more space and can be read more easily.
2026-04-08 19:10:03 +02:00
Markus Fisch afd6c74c80 Verify IDB barcodes
Using the german CSCA master list.
2026-04-08 19:09:52 +02:00
Markus Fisch 5ffea2edd8 Fix detecting web URLs with a subdomain 2026-04-08 19:50:05 +02:00
Markus Fisch 78d62395e2 Add missing translations for IDL/EPC labels 2026-04-08 19:38:33 +02:00
Markus Fisch a5d1465528 Remove unnecessary EpcQrInfo wrapper class
And return the map directly.
2026-04-08 19:16:19 +02:00
Markus Fisch 14517bad17 Add vdstools to parse VDS and IDB barcodes
Without verifying yet.
2026-04-08 19:14:15 +02:00
Markus Fisch 7a5e07fb3c Handle deep link in onNewIntent for EncodeActivity
Calendar apps reuse the existing task instead of creating a
new one, so Android calls onNewIntent() rather than onCreate().
Without an onNewIntent() override the intent's query parameters
(format, content, execute) were silently ignored.

Extract intent application into applyIntent() and call it from
both onCreate() and a new onNewIntent() override.
2026-04-07 20:11:52 +02:00
Markus Fisch c6b3ca08f4 Restrict scans to horizontal crosshair
Only accept camera scan results that intersect the horizontal
crosshair when crosshairs are enabled. For 1D linear barcodes,
expand collapsed position lines into a narrow band first so the
same rule works reliably for formats that do not report full
symbol bounds.
2026-04-07 21:34:26 +02:00
Markus Fisch 73047cdee6 Simplify old to new format name translation
To have that list only once.
2026-04-07 19:23:56 +02:00
Markus Fisch bec4aa3120 Replace Code39 with the more specific Code39Std
So users get only Code39Std when they restrict reading to
this format.
2026-04-07 18:17:25 +02:00
Markus Fisch eaaaaf14c2 Add barcode format descriptions
To DecodeActivity and EncodeActivity.

Enables/Disables with meta data.
2026-04-07 20:59:10 +02:00
Markus Fisch 971742aff4 Fix barcode format preference summary
Sort the selected barcode formats alphabetically in the
preferences summary and map stored values back to their
display labels.
2026-04-07 15:46:47 +02:00
Markus Fisch 28915eacdb Simplify setting preference summaries 2026-04-06 23:37:35 +02:00
Markus Fisch a2c1d11b5b Include barcode format in history search
So we can use for barcode formats too.
2026-04-06 23:09:04 +02:00
Markus Fisch e9dd396f34 Add a dedicated barcode icon for PDF417 2026-04-07 12:02:08 +02:00
Markus Fisch c7edba746c Advance version number to 1.70.0 2026-04-03 23:39:44 +02:00
Markus Fisch 34c8dfd169 Add barcode format icons in history 2026-04-03 23:34:45 +02:00
bovirusandGitHub 459980899f Italian language update 2026-04-03 23:10:31 +02:00
Markus Fisch 2e0693cb45 Add beep audio stream preference
Let users choose whether confirmation beeps use the media or
notification audio stream, with media as the default.

Recreate the tone generator when the selected stream changes so the
new preference takes effect immediately, and add the required
localized labels for the new setting.
2026-04-03 12:00:14 +02:00
Markus Fisch 187825932e Parse EPC-QR codes and show the data 2026-04-01 21:30:51 +02:00
Markus Fisch a80802b4bc Improve layout of decode screen
Move the things a user cares most about to the top of the
screen, and everything else to the bottom.
2026-04-01 20:58:02 +02:00
Markus Fisch aa589a5d53 Improve size of generated PDF417 barcodes
PDF417 requires a bit more horizontal width because it
has many modules and doesn't fit into a square shape.
2026-04-01 20:20:46 +02:00
Markus Fisch 6a42e54f0b Fix parsing first field of subfile in IDLParser
Per the AAMVA spec, each subfile in the data section starts with its
2-char type marker (e.g. "ZV", "ZI") directly prepended to the first
field, with no separator between them. The key-value parser had no
awareness of this, so the marker was absorbed into the field key
(e.g. "ZVA" was parsed as "ZVZ" with value "VA01" instead of "01").
2026-04-01 19:55:30 +02:00
Markus Fisch e134b18bba Use quiet zone from library
Which uses zint for linear barcodes but avoids it for all
other types because zint doesn't generate reasonable margins
for other barcodes.

This fixes too thin margins for linear barcodes.
2026-03-31 22:27:59 +02:00
bovirusandGitHub a112484dcb Italian language update 2026-03-27 12:16:26 +01:00
Markus Fisch 4f36030e38 Advance version number to 1.69.1 2026-03-26 19:35:24 +01:00
Yurt PageandMarkus Fisch 90c2c03d12 Update translations 2026-03-26 19:30:24 +01:00
Markus Fisch 3f853be51a Advance version number to 1.69.0 2026-03-26 19:30:03 +01:00
Markus Fisch 835a348fad Fix confirm beep by switching to STREAM_MUSIC
STREAM_NOTIFICATION volume can be zero independently of ringer
mode, so isSilent() would pass but no sound played. STREAM_MUSIC
is the correct stream for in-app sound effects and is not                       affected by this.
2026-03-26 19:30:03 +01:00
Markus Fisch 198a35fc15 Clean up fragments to activities transition 2026-03-26 19:30:03 +01:00
Markus Fisch 6be1b25c38 Add format restrictions to automated actions
And ignored codes too.
2026-03-26 19:30:03 +01:00
Markus Fisch ac632e3aac Restrict searching to a specific format 2026-03-26 19:30:03 +01:00
Markus Fisch ddc5a14558 Clean up naming of search term dialog 2026-03-26 19:30:01 +01:00
Markus Fisch 8b1ffe3e5a Add preference for codes that should not be saved
So short-lived one-shot links like FIDO:// URLs don't
end up in the history.
2026-03-26 19:29:19 +01:00
Markus Fisch e9ba66848f Add an option to show crosshairs 2026-03-26 19:29:19 +01:00
Markus Fisch 7bdef27a50 Pin history items
So it's easy to keep important scans at the top.
2026-03-23 22:14:26 +01:00
Markus Fisch ccd16a0f95 Improve preference icons
And add better ones where appropriate.
2026-03-21 17:51:02 +01:00
Markus Fisch a385678de1 Optimize vector drawables 2026-03-21 17:32:31 +01:00
Markus Fisch 43c597f6fb Add leak canary for debugging 2026-03-21 17:28:09 +01:00
Markus Fisch ecccf34418 Advance version number to 1.68.7 2026-03-20 21:56:20 +01:00
Markus Fisch 4027ce2cc6 Reformat new code 2026-03-20 21:56:07 +01:00
Markus Fisch 209e6c5c30 Add icons to preferences screen
Assign existing action icons to preference rows in the settings
screen so related options are easier to scan.
2026-03-20 21:55:37 +01:00
Markus Fisch 5e29f3fbe9 Update kotlin version 2026-03-20 21:26:05 +01:00
Markus Fisch ba3d63277a Update gradle wrapper 2026-03-20 21:15:56 +01:00
Markus Fisch fe34b7209d Fix quiet zone for recreated barcodes
Older history entries can miss the stored symbol matrix and
therefore fall back to re-encoding the barcode content.

After the ZXingCpp 3 migration, that fallback used a zero margin,
so recreated barcodes from those records lost their quiet zone.
2026-03-20 21:08:20 +01:00
Markus Fisch 0efdca7b4a Remove unnecessary null checking of error message
Because it's always non-null now.
2026-03-20 15:46:38 +01:00
Markus Fisch 827fbedc1f Fix automatic screen brightening
And remove lateinit from `brightenScreenItem` to check
if it's null or not.

Also delay automatic brightening to give Android a bit
more time to sort things out.
2026-03-20 15:46:38 +01:00
Markus Fisch 107b5bafd4 Update history listing on resume
To immediately show label changes, for example.
2026-03-20 15:46:38 +01:00
Markus Fisch 19a71480fa Update tools version 2026-03-20 15:46:38 +01:00
Markus Fisch 909e97a591 Map zoom bar to zoom ratio instead of linear zoom
Linear zoom 0 switches to the ultra-wide lens on devices
that support it, causing blurry images. Mapping to zoom ratios
from 1.0x to maxZoomRatio keeps the main lens in use.
2026-03-17 21:54:34 +01:00
Markus Fisch bc2be96871 Simplify updating the flash FAB
It's much simpler if we invert the availability parameter.
2026-03-20 10:53:39 +01:00
Markus Fisch d834621d99 Reset stored zoom level and always start at 0
Because the previous zoom levels were for the legacy camera API
and they don't carry over to CameraX.

With CameraX, it's also better to start with no zoom.
2026-03-20 10:53:39 +01:00
Markus Fisch ad83be1a60 Use ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY
Which greatly improves the initial sharpness of the camera image.

Without this, despite autofocus, the initial camera frame can be
a bit blurry on some devices.
2026-03-20 10:53:39 +01:00
Markus Fisch d65ea02b49 Focus on taps but not on swipes 2026-03-20 10:53:32 +01:00
Markus Fisch c8d1357ceb Use CONTINUOUS_PICTURE AF
CameraX defaults to CONTINUOUS_VIDEO autofocus, which trades
focus accuracy for smooth transitions. For barcode scanning we
need the sharpest possible lock, so switch to CONTINUOUS_PICTURE.

But this still isn't enough to have the same quick autofocus the
legacy API had.
2026-03-20 10:52:44 +01:00
Markus Fisch 4c05d046e6 Set CONTROL_SCENE_MODE_BARCODE
To configure the camera for barcode reading (macro focus range,
disabled stabilisation). Same way the legacy Camera API's
SCENE_MODE_BARCODE did.

Unfortunately, this mode isn't supported by some camera drivers
and it doesn't really improve focusing near barcodes on my test
devices.
2026-03-20 10:52:44 +01:00
Markus Fisch 4423ee62c8 Migrate to CameraX
Since the app is on minSDK 21 now, and because some camera
drivers apparently don't support the torch feature through
the legacy API.
2026-03-20 10:52:21 +01:00
Markus Fisch bc69f99f8b Advance version number to 1.68.6 2026-03-14 14:53:30 +01:00
Markus Fisch 6b088031fd Remove unused plural quantities
Delete plural forms that Android never selects for the affected
locales, based on the lint report.
2026-03-13 22:43:31 +01:00
Markus Fisch b27d03e9c0 Ask for confirmation before deleting a scan
From the decode view.
2026-03-13 22:37:43 +01:00
Markus Fisch 9a13838f28 Fix RTL hardcoded layout attributes
Remove redundant left/right layout attributes and keep the
start/end variants that match RTL-aware behavior.
2026-03-13 22:34:08 +01:00
Markus Fisch cce2a460f6 Update material library 2026-03-12 23:34:05 +01:00
Markus Fisch 36352ecde2 Apply KTX extensions from lint
Replace lint-flagged platform calls with their Kotlin extension
equivalents and add the core-ktx dependency they rely on.
2026-03-13 16:33:54 +01:00
Markus Fisch 9a0bc324cb Migrate support libraries to AndroidX
Replace legacy support dependencies and namespaces with AndroidX and
Material equivalents so the app builds on the modern stack again.

Update Kotlin imports, XML widget class names, and manifest provider
references, then fix AndroidX nullability fallout and missing lifecycle
super calls reported by lint.
2026-03-12 23:19:02 +01:00
Markus Fisch 6dabafd6a3 Replace fragments with standalone activities
Remove MainActivity as a single fragment host and convert each
fragment (Barcode, Decode, Encode, History, Profiles, Automated
Actions, Network Suggestions) into its own Activity.

Update CameraActivity to launch each Activity directly instead of
routing through MainActivity intent factories.
2026-03-12 23:10:18 +01:00
Markus Fisch dde4b1e0bd Update gradle wrapper 2026-03-12 22:58:58 +01:00
Markus Fisch 8a8c7bd1ff Remove values-v21 for minSDK is 21 now
No longer required. Merged the Button style with
`res/values/styles.xml`.
2026-03-12 22:16:02 +01:00
Markus Fisch a2aed42e25 Remove Scan parcel compatibility code
Stop transporting Scan as a Parcelable and keep only the
bundle-based handoff for decoded results.

This removes version-sensitive parcel decoding entirely and makes
scan navigation stable across app updates.
2026-03-12 21:51:55 +01:00
Markus Fisch a38b7b647f Use bundles for decoded scan handoff
Replace Scan parcelables in the decode navigation flow with plain
Bundle data.

This avoids BadParcelableException when Android lazily unparcels a
Scan written with a different layout or legacy ZXingCpp format names.
A bundle of primitives is stable across app updates and does not depend
on custom parcel compatibility.
2026-03-12 21:44:57 +01:00
Markus Fisch 76e82ed842 Migrate to ZXingCpp 3.x and raise minSdk to 21
Which is required (to build and run) version 3 of ZXingCpp,
because ZXingCpp requires C++20, which in turn requires an
NDK with minSdk 21.

So to use the new version, we need to raise the minSdk
and cut off old devices just because C++17 didn't cut the
mustard anymore. Well, I guess this had to happen at some
point, and because SDK 21 is 12 years old now, it's probably
even reasonable to raise the bar.
2026-03-12 20:53:23 +01:00
Markus Fisch fd54bc604e Advance version number to 1.68.5 2026-03-02 19:07:06 +01:00
Markus Fisch 59b62ecbc4 Revert ZXingC++ to v2.3.0.4
Because 3.x requires C++20, what requires minSdk 21.

While ZXingC++ 3.x compiles and runs on some devices (with the
current minSdk), the library does crash on other devices.

So updating to ZXingC++ 3.x requires more work, unfortunately.

This reverts commit
a90bdad117
4b2eb80612
7a59126312
2026-03-02 18:59:41 +01:00
Markus Fisch de6a2863f3 Advance version number to 1.68.4 2026-02-24 23:57:27 +01:00
Markus Fisch 4b2eb80612 Fix margin for 1D barcodes
Since ZXingCpp v3.0.0+ changed the default from -1 to 0.

Using -1 results in an exception in `BitMatrix::setRegion()`
because of invalid top/left coordinates.
2026-02-24 23:52:04 +01:00
Markus Fisch 2de40d2277 Advance version number to 1.68.3 2026-02-23 23:13:10 +01:00
Markus Fisch 7673b7cb0d Fallback to getParcelableExtra(name)
When getting the DECODE extra.

`getParcelableExtra(name, Class)` can throw on some versions
of Android when the ClassLoader associated with the Bundle's
lazy-decoded value is null in certain scenarios (e.g., after
an activity restart where the system re-delivers the intent).
Simply fallback to the "deprecated" `getParcelableExtra(name)`
which works in this case, because it doesn't verify the
parcelable's class against Scan::class.java.
2026-02-23 21:00:30 +01:00
Markus Fisch 097c5e4453 Improve getting fragment for MainActivity
And properly finish the MainActivity for malformed intents.
2026-02-23 17:21:30 +01:00
247 changed files with 9431 additions and 2665 deletions
+78
View File
@@ -1,5 +1,83 @@
# Change Log
## 1.73.1
* Update ZXingCpp and support Telepen format
* Update vdstools
* Fix stale history row highlighting
## 1.73.0
* Support Material You and Dynamic Colors
* Strip common tracking parameters before opening scanned URLs
* Parse and display otpauth:// code fields
* Update Italian and Latvian language
* Update BouncyCastle and vdstools library
* Fix deeplink argument parsing
## 1.72.2
* Import/export preferences using QR codes
* Add predefined expert and simple profiles
* Update ZXingCpp library
* Fix beeping on scans
## 1.72.1
* Fix migrating DATA_BAR entries
* Italian language update
## 1.72.0
* Improve result screen
* Update vdstools
* Fix camera initialization
## 1.71.0
* Parse VDS and IDB barcodes
* Restrict scanning to horizontal crosshair if set
* Include barcode format in history search
* Add a dedicated barcode icon for PDF417
* Add barcode format descriptions
* Fix handling deep links
* Fix detecting web URLs
## 1.70.0
* Add barcode format icons in history
* Add beep audio stream preference
* Parse EPC-QR codes and show the data
* Improve layout of decode screen
* Fix parsing first field of subfile in IDLParser
* Use quiet zone from ZXingCpp
* Italian language update
## 1.69.1
* Update translations
## 1.69.0
* Add an option to show crosshairs
* Add preference for codes that should not be saved
* Add option to restrict searching to a specific format
* Pin history items
* Improve preference icons
* Optimize vector drawables
* Fix beeping when phone is set to Vibrate mode
## 1.68.7
* Add icons to preferences screen
* Update history listing on resume
* Fix quiet zone for recreated barcodes
* Fix automatic screen brightening
* Migrate to CameraX
## 1.68.6
* Update ZXingCpp
* Ask for confirmation before deleting a scan from the result screen
## 1.68.5
* Downgrade ZXingCpp for compatibilty
## 1.68.4
* Fix generating 1D barcodes
## 1.68.3
* Stability improvement
## 1.68.2
* Fix barcode regeneration
+2
View File
@@ -59,6 +59,7 @@ ZXing can read the following barcode formats:
* [ITF][itf]
* [MAXICODE][maxicode] (partial)
* [PDF417][pdf417]
* [Telepen][telepen]
* [QR CODE][qr_code]
* [Micro QR Code][micro_qr_code]
* [rMQR Code][rmqr_code]
@@ -207,6 +208,7 @@ In order to set up Bluetooth forwarding to a computer:
[itf]: https://en.wikipedia.org/wiki/Interleaved_2_of_5
[maxicode]: https://en.wikipedia.org/wiki/MaxiCode
[pdf417]: https://en.wikipedia.org/wiki/PDF417
[telepen]: https://en.wikipedia.org/wiki/Telepen
[qr_code]: https://en.wikipedia.org/wiki/QR_code
[micro_qr_code]: https://en.wikipedia.org/wiki/QR_code#Micro_QR_code
[rmqr_code]: https://www.qrcode.com/en/codes/rmqr.html
+21 -11
View File
@@ -5,11 +5,11 @@ android {
compileSdk sdk_version
defaultConfig {
minSdk 9
minSdk 21
targetSdk sdk_version
versionCode 156
versionName '1.68.2'
versionCode 170
versionName '1.73.1'
}
signingConfigs {
@@ -67,20 +67,30 @@ android {
dependencies {
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0"
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.8.0'
implementation "androidx.core:core-ktx:1.17.0" // Keep for minSDK 21.
// Support
implementation "com.android.support:appcompat-v7:$support_version"
implementation "com.android.support:design:$support_version"
implementation "com.android.support:preference-v7:$support_version"
implementation "com.android.support:preference-v14:$support_version"
implementation "androidx.appcompat:appcompat:1.7.1"
implementation "com.google.android.material:material:1.13.0" // Keep for minSDK 21.
implementation "androidx.preference:preference:1.2.1"
// External
implementation 'com.github.markusfisch:CameraView:1.10.0'
implementation 'com.github.markusfisch:ScalingImageView:1.4.1'
implementation 'com.github.markusfisch:zxing-cpp:v3.0.2.0'
var camerax_version = '1.4.2' // Keep for minSDK 21.
implementation "androidx.camera:camera-core:$camerax_version"
implementation "androidx.camera:camera-camera2:$camerax_version"
implementation "androidx.camera:camera-lifecycle:$camerax_version"
implementation "androidx.camera:camera-view:$camerax_version"
implementation 'com.github.markusfisch:ScalingImageView:1.4.3'
implementation 'com.github.markusfisch:zxing-cpp:v3.0.2.6'
implementation 'de.tsenger:vdstools:0.17.0'
implementation 'org.bouncycastle:bcpkix-jdk15to18:1.84'
// Testing
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.16.1'
// Debugging
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
}
Binary file not shown.
+2
View File
@@ -3,3 +3,5 @@
-dontwarn android.support.v8.**
-keep class android.support.v7.widget.SearchView { *; }
-keep class de.tsenger.vdstools.** { *; }
+27 -3
View File
@@ -21,7 +21,7 @@
<uses-feature android:name="android.hardware.camera"/>
<uses-feature android:name="android.hardware.camera.autofocus"
android:required="false" />
<uses-sdk tools:overrideLibrary="android.support.v14.preference"/>
<uses-sdk tools:overrideLibrary="androidx.preference"/>
<queries>
<intent>
<action android:name="android.intent.action.VIEW"/>
@@ -112,7 +112,13 @@
android:name=".activity.WelcomeActivity"
android:exported="true"/>
<activity
android:name=".activity.MainActivity"
android:name=".activity.PreferencesActivity"
android:exported="true"/>
<activity
android:name=".activity.HistoryActivity"
android:exported="true"/>
<activity
android:name=".activity.EncodeActivity"
android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
@@ -130,6 +136,24 @@
<data android:scheme="binaryeye" android:host="encode"/>
</intent-filter>
</activity>
<activity
android:name=".activity.DecodeActivity"
android:exported="false"/>
<activity
android:name=".activity.BarcodeActivity"
android:exported="false"/>
<activity
android:name=".activity.ProfilesActivity"
android:exported="false"/>
<activity
android:name=".activity.AutomatedActionsActivity"
android:exported="false"/>
<activity
android:name=".activity.IgnoreCodesActivity"
android:exported="false"/>
<activity
android:name=".activity.NetworkSuggestionsActivity"
android:exported="false"/>
<service
android:name=".service.ScanTileService"
android:icon="@drawable/ic_action_scan"
@@ -142,7 +166,7 @@
</intent-filter>
</service>
<provider
android:name="android.support.v4.content.FileProvider"
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
Binary file not shown.
@@ -2,15 +2,15 @@ package de.markusfisch.android.binaryeye.actions
import android.content.Context
import android.content.Intent
import de.markusfisch.android.binaryeye.content.execShareIntent
import de.markusfisch.android.binaryeye.content.openUrl
import de.markusfisch.android.binaryeye.content.startIntentOrToast
import de.markusfisch.android.binaryeye.widget.toast
abstract class Action {
abstract val iconResId: Int
abstract val titleResId: Int
var fired: Boolean = false
var executed: Boolean = false
abstract fun canExecuteOn(data: ByteArray): Boolean
abstract suspend fun execute(context: Context, data: ByteArray)
@@ -24,7 +24,7 @@ abstract class IntentAction : Action() {
if (intent == null) {
context.toast(errorMsg)
} else {
fired = context.execShareIntent(intent)
executed = context.startIntentOrToast(intent)
}
}
@@ -52,6 +52,6 @@ abstract class SchemeAction : Action() {
}
final override suspend fun execute(context: Context, data: ByteArray) {
fired = context.openUrl(String(data))
executed = context.openUrl(String(data))
}
}
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.actions.mail
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.core.net.toUri
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IntentAction
@@ -24,7 +24,7 @@ object MailAction : IntentAction() {
)?.groupValues?.get(1) ?: return null
return Intent(
Intent.ACTION_SENDTO,
Uri.parse("mailto:$mailWithMessage")
"mailto:$mailWithMessage".toUri()
)
}
}
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.actions.mail
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.core.net.toUri
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IntentAction
@@ -26,7 +26,7 @@ object MatMsgAction : IntentAction() {
}
return Intent(
Intent.ACTION_SENDTO,
Uri.parse("mailto:")
"mailto:".toUri()
).apply {
mm.to?.let {
putExtra(Intent.EXTRA_EMAIL, arrayOf(mm.to))
@@ -15,7 +15,7 @@ object OpenOrSearchAction : Action() {
override fun canExecuteOn(data: ByteArray): Boolean = false
override suspend fun execute(context: Context, data: ByteArray) {
fired = openUrlOrSearch(context, String(data))
executed = openUrlOrSearch(context, String(data))
}
}
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.actions.sms
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.core.net.toUri
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IntentAction
@@ -27,7 +27,7 @@ object SmsAction : IntentAction() {
} ?: return null
return Intent(
Intent.ACTION_SENDTO,
Uri.parse("smsto:$number")
"smsto:$number".toUri()
).apply {
if (message.isNotEmpty()) {
putExtra("sms_body", message)
@@ -3,7 +3,6 @@ package de.markusfisch.android.binaryeye.actions.vtype.vcard
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.os.Build
import android.provider.ContactsContract
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IntentAction
@@ -98,38 +97,36 @@ object VCardAction : IntentAction() {
}
putExtra(ContactsContract.Intents.Insert.POSTAL, addr.value.locationFormat)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
val urls = info["URL"]?.mapNotNull {
it.value.takeIf { url -> url.isNotBlank() }?.let { url ->
ContentValues(2).apply {
put(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE
)
put(ContactsContract.CommonDataKinds.Website.URL, url)
}
val urls = info["URL"]?.mapNotNull {
it.value.takeIf { url -> url.isNotBlank() }?.let { url ->
ContentValues(2).apply {
put(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE
)
put(ContactsContract.CommonDataKinds.Website.URL, url)
}
}
val bDays = info["BDAY"]?.mapNotNull {
it.value.takeIf { day -> day.isNotBlank() }?.let { day ->
ContentValues(3).apply {
put(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE
)
put(
ContactsContract.CommonDataKinds.Event.TYPE,
ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY
)
put(ContactsContract.CommonDataKinds.Event.START_DATE, day)
}
}
}
putParcelableArrayListExtra(
ContactsContract.Intents.Insert.DATA,
ArrayList(listOfNotNull(urls, bDays).flatten())
)
}
val bDays = info["BDAY"]?.mapNotNull {
it.value.takeIf { day -> day.isNotBlank() }?.let { day ->
ContentValues(3).apply {
put(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE
)
put(
ContactsContract.CommonDataKinds.Event.TYPE,
ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY
)
put(ContactsContract.CommonDataKinds.Event.START_DATE, day)
}
}
}
putParcelableArrayListExtra(
ContactsContract.Intents.Insert.DATA,
ArrayList(listOfNotNull(urls, bDays).flatten())
)
}
}
@@ -2,9 +2,7 @@ package de.markusfisch.android.binaryeye.actions.vtype.vevent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.provider.CalendarContract
import android.support.annotation.RequiresApi
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IntentAction
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
@@ -21,14 +19,10 @@ object VEventAction : IntentAction() {
get() = R.string.vevent_failed
override fun canExecuteOn(data: ByteArray): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
return false
}
val type = VTypeParser.parseVType(String(data))
return type == "VEVENT" || type == "VCALENDAR"
}
@RequiresApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
val info = VTypeParser.parseMap(String(data))
@@ -7,7 +7,7 @@ import de.markusfisch.android.binaryeye.content.openUrl
object WebAction : Action() {
private val colloquialRegex =
"^(URL:[ ]*)*(http[s]*://)*[A-Za-z0-9-]{3,}\\.[A-Za-z]{2,}[^ \t\r\n]*$".toRegex()
"^(URL:[ ]*)*(http[s]*://)*[A-Za-z0-9.-]{3,}\\.[A-Za-z]{2,}[^ \t\r\n]*$".toRegex()
override val iconResId: Int = R.drawable.ic_action_open
override val titleResId: Int = R.string.open_url
@@ -24,6 +24,6 @@ object WebAction : Action() {
if (!url.startsWith("http") && !url.startsWith("ftp")) {
url = "http://${url}"
}
fired = context.openUrl(url)
executed = context.openUrl(url)
}
}
@@ -23,12 +23,12 @@ object WifiAction : Action() {
override suspend fun execute(context: Context, data: ByteArray) {
withContext(Dispatchers.IO) {
fired = WifiConnector.parse(String(data))?.let {
executed = WifiConnector.parse(String(data))?.let {
WifiConnector.addNetwork(context, it)
} ?: false
withContext(Dispatchers.Main) {
context.toast(
if (fired) {
if (executed) {
R.string.wifi_added
} else {
R.string.wifi_config_failed
@@ -8,7 +8,7 @@ import android.net.wifi.WifiManager
import android.net.wifi.WifiNetworkSuggestion
import android.os.Build
import android.provider.Settings
import android.support.annotation.RequiresApi
import androidx.annotation.RequiresApi
import java.util.Locale
/**
@@ -113,34 +113,30 @@ object WifiConnector {
get() = inputMap["I"] ?: ""
internal val eapMethod: Int?
get() = if (inputMap["E"].isNullOrEmpty()) {
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
WifiEnterpriseConfig.Eap.NONE
}
WifiEnterpriseConfig.Eap.NONE
} else when (inputMap["E"]?.uppercase()) {
"AKA" -> requireSdk(Build.VERSION_CODES.LOLLIPOP) { WifiEnterpriseConfig.Eap.AKA }
"AKA" -> WifiEnterpriseConfig.Eap.AKA
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.M) { WifiEnterpriseConfig.Eap.AKA_PRIME }
"NONE" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.NONE }
"PEAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.PEAP }
"PWD" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.PWD }
"SIM" -> requireSdk(Build.VERSION_CODES.LOLLIPOP) { WifiEnterpriseConfig.Eap.SIM }
"TLS" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.TLS }
"TTLS" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.TTLS }
"NONE" -> WifiEnterpriseConfig.Eap.NONE
"PEAP" -> WifiEnterpriseConfig.Eap.PEAP
"PWD" -> WifiEnterpriseConfig.Eap.PWD
"SIM" -> WifiEnterpriseConfig.Eap.SIM
"TLS" -> WifiEnterpriseConfig.Eap.TLS
"TTLS" -> WifiEnterpriseConfig.Eap.TTLS
"UNAUTH_TLS" -> requireSdk(Build.VERSION_CODES.N) { WifiEnterpriseConfig.Eap.UNAUTH_TLS }
else -> null
}
internal val phase2Method: Int?
get() = if (inputMap["PH2"].isNullOrEmpty()) {
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
WifiEnterpriseConfig.Phase2.NONE
}
WifiEnterpriseConfig.Phase2.NONE
} else when (inputMap["PH2"]?.uppercase()) {
"AKA" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA }
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA_PRIME }
"GTC" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.GTC }
"MSCHAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.MSCHAP }
"MSCHAPV2" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.MSCHAPV2 }
"NONE" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.NONE }
"PAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.PAP }
"GTC" -> WifiEnterpriseConfig.Phase2.GTC
"MSCHAP" -> WifiEnterpriseConfig.Phase2.MSCHAP
"MSCHAPV2" -> WifiEnterpriseConfig.Phase2.MSCHAPV2
"NONE" -> WifiEnterpriseConfig.Phase2.NONE
"PAP" -> WifiEnterpriseConfig.Phase2.PAP
"SIM" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.SIM }
else -> null
}
@@ -265,7 +261,7 @@ object WifiConnector {
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP)
}
"WPA2-EAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
"WPA2-EAP" -> {
passwordWithQuotes?.let {
preSharedKey = it
} ?: return null
@@ -285,7 +281,7 @@ object WifiConnector {
data.eapMethod ?: return null // non valid eapMethod
enterpriseConfig.phase2Method =
data.phase2Method ?: return null // non valid phase2Method
} ?: return null // api isn't high enough
}
}
return this
}
@@ -0,0 +1,65 @@
package de.markusfisch.android.binaryeye.activity
import android.content.Context
import android.content.pm.PackageManager
import android.view.Menu
import android.view.MenuInflater
import androidx.annotation.LayoutRes
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isNotEmpty
import de.markusfisch.android.binaryeye.app.PERMISSION_LOCATION
import de.markusfisch.android.binaryeye.app.PERMISSION_WRITE
import de.markusfisch.android.binaryeye.app.applyLocale
import de.markusfisch.android.binaryeye.app.permissionGrantedCallback
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.view.initBars
abstract class AbstractBaseActivity : AppCompatActivity() {
override fun attachBaseContext(base: Context?) {
base?.applyLocale(prefs.customLocale)
super.attachBaseContext(base)
}
protected fun setScreenContentView(@LayoutRes layoutId: Int) {
setContentView(layoutId)
initBars()
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
override fun onSupportNavigateUp(): Boolean {
finish()
return true
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(
requestCode,
permissions,
grantResults
)
when (requestCode) {
PERMISSION_LOCATION, PERMISSION_WRITE -> {
if (grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED
) {
permissionGrantedCallback?.invoke()
permissionGrantedCallback = null
}
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
onCreateOptionsMenu(menu, menuInflater)
return menu.isNotEmpty() || super.onCreateOptionsMenu(menu)
}
protected open fun onCreateOptionsMenu(
menu: Menu,
inflater: MenuInflater
) = Unit
}
@@ -1,49 +1,41 @@
package de.markusfisch.android.binaryeye.fragment
package de.markusfisch.android.binaryeye.activity
import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.ListView
import android.widget.RadioGroup
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.automation.AutomatedAction
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
class AutomatedActionsFragment : Fragment() {
class AutomatedActionsActivity : AbstractBaseActivity() {
private val actions = ArrayList<AutomatedAction>()
private lateinit var adapter: ActionsAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?
): View? {
val ac = activity ?: return null
ac.setTitle(R.string.automated_actions)
val view = inflater.inflate(
R.layout.fragment_automated_actions,
container,
false
)
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setScreenContentView(R.layout.activity_automated_actions)
setTitle(R.string.automated_actions)
actions.clear()
actions.addAll(prefs.automatedActions)
val listView = view.findViewById<ListView>(R.id.actions)
listView.emptyView = view.findViewById(R.id.no_actions)
adapter = ActionsAdapter(ac, actions)
val listView = findViewById<ListView>(R.id.actions)
listView.emptyView = findViewById(R.id.no_actions)
adapter = ActionsAdapter(this, actions)
listView.adapter = adapter
listView.setOnScrollListener(systemBarListViewScrollListener)
listView.setOnItemClickListener { _, _, position, _ ->
@@ -54,24 +46,22 @@ class AutomatedActionsFragment : Fragment() {
true
}
view.findViewById<View>(R.id.add).setOnClickListener {
findViewById<View>(R.id.add).setOnClickListener {
editAction(null)
}
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
listView.setPaddingFromWindowInsets()
return view
}
private fun editAction(position: Int?) {
val ac = activity ?: return
val action = position?.let { actions[it] }
val view = ac.layoutInflater.inflate(
val view = layoutInflater.inflate(
R.layout.dialog_automated_action,
null
)
val regexView = view.findViewById<EditText>(R.id.regex)
val formatView = view.findViewById<Spinner>(R.id.format)
val urlView = view.findViewById<EditText>(R.id.url_template)
val typeGroup = view.findViewById<RadioGroup>(R.id.action_type)
val urlSection = view.findViewById<View>(R.id.url_section)
@@ -79,9 +69,31 @@ class AutomatedActionsFragment : Fragment() {
val intentUriView = view.findViewById<EditText>(
R.id.intent_uri_template
)
val names = resources.getStringArray(
R.array.barcode_formats_names
).toMutableList().apply {
add(0, getString(R.string.all_formats))
}
val values = resources.getStringArray(
R.array.barcode_formats_values
).toMutableList().apply {
add(0, "")
}
formatView.adapter = ArrayAdapter(
this,
android.R.layout.simple_spinner_item,
names
).apply {
setDropDownViewResource(
android.R.layout.simple_spinner_dropdown_item
)
}
if (action != null) {
regexView.setText(action.pattern)
formatView.setSelection(
values.indexOf(action.format ?: "").coerceAtLeast(0)
)
when (action.type) {
AutomatedAction.Type.Intent -> {
urlView.setText(action.urlTemplate ?: "")
@@ -91,6 +103,8 @@ class AutomatedActionsFragment : Fragment() {
intentUriView.setText(action.intentUriTemplate ?: "")
}
}
} else {
formatView.setSelection(0)
}
fun updateTypeSections(checkedId: Int) {
@@ -112,7 +126,7 @@ class AutomatedActionsFragment : Fragment() {
typeGroup.check(initialTypeId)
updateTypeSections(initialTypeId)
val dialog = AlertDialog.Builder(ac)
val dialog = MaterialAlertDialogBuilder(this)
.setTitle(
if (action == null) {
R.string.automated_action_add
@@ -130,6 +144,8 @@ class AutomatedActionsFragment : Fragment() {
addOrUpdateAction(
position,
regexView,
formatView,
values,
urlView,
typeGroup,
intentUriView,
@@ -144,6 +160,8 @@ class AutomatedActionsFragment : Fragment() {
private fun addOrUpdateAction(
position: Int?,
regexView: EditText,
formatView: Spinner,
values: List<String>,
urlView: EditText,
typeGroup: RadioGroup,
intentUriView: EditText,
@@ -161,6 +179,9 @@ class AutomatedActionsFragment : Fragment() {
return
}
val isIntent = typeGroup.checkedRadioButtonId == R.id.action_type_intent
val format = formatView.selectedItemPosition.let {
values[it].ifEmpty { null }
}
val newAction = if (isIntent) {
val urlTemplate = urlView.text.toString().trim()
if (urlTemplate.isEmpty()) {
@@ -170,6 +191,7 @@ class AutomatedActionsFragment : Fragment() {
AutomatedAction(
pattern = pattern,
type = AutomatedAction.Type.Intent,
format = format,
urlTemplate = urlTemplate
)
} else {
@@ -193,6 +215,7 @@ class AutomatedActionsFragment : Fragment() {
AutomatedAction(
pattern = pattern,
type = AutomatedAction.Type.CustomIntent,
format = format,
intentUriTemplate = intentUriTemplate
)
}
@@ -207,8 +230,7 @@ class AutomatedActionsFragment : Fragment() {
}
private fun confirmRemoveAction(position: Int) {
val ac = activity ?: return
AlertDialog.Builder(ac)
MaterialAlertDialogBuilder(this)
.setMessage(R.string.automated_action_remove_confirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
actions.removeAt(position)
@@ -259,6 +281,9 @@ class AutomatedActionsFragment : Fragment() {
)
}
}
action.format?.let { format ->
subtitle.text = "${subtitle.text} (${format.prettifyFormatName()})"
}
}
return view
}
@@ -1,23 +1,21 @@
package de.markusfisch.android.binaryeye.fragment
package de.markusfisch.android.binaryeye.activity
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Matrix
import android.os.Build
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView
import android.widget.SeekBar
import android.widget.TextView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.db
import de.markusfisch.android.binaryeye.app.hasWritePermission
@@ -55,7 +53,7 @@ import java.util.Locale
import kotlin.math.max
import kotlin.math.min
class BarcodeFragment : Fragment() {
class BarcodeActivity : AbstractBaseActivity() {
private enum class FileType {
PNG, JPG, SVG, TXT
}
@@ -65,36 +63,24 @@ class BarcodeFragment : Fragment() {
private lateinit var imageView: ConfinedScalingImageView
private lateinit var barcode: Barcode<*>
private lateinit var addToHistoryItem: MenuItem
private lateinit var brightenScreenItem: MenuItem
private var contentView: View? = null
private var addToHistoryItem: MenuItem? = null
private var brightenScreenItem: MenuItem? = null
private var currentBrightness = -1f
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?
): View? {
val ac = activity ?: return null
ac.setTitle(R.string.view_barcode)
val view = inflater.inflate(
R.layout.fragment_barcode,
container,
false
)
setScreenContentView(R.layout.activity_barcode)
setTitle(R.string.view_barcode)
contentView = findViewById(android.R.id.content)
// Make `imageView` available for `onPause()`.
imageView = view.findViewById(R.id.barcode)
imageView = findViewById(R.id.barcode)
val bitmap: Bitmap
try {
barcode = arguments?.toBarcode() ?: throw IllegalArgumentException(
barcode = intent?.extras?.toBarcode() ?: throw IllegalArgumentException(
"Illegal arguments"
)
// Catch exceptions from encoding.
@@ -104,11 +90,9 @@ class BarcodeFragment : Fragment() {
if (message.isNullOrEmpty()) {
message = getString(R.string.error_encoding_barcode)
}
message?.let {
ac.toast(message)
}
fragmentManager.popBackStack()
return null
toast(message)
finish()
return
}
imageView.setImageBitmap(bitmap)
@@ -128,7 +112,7 @@ class BarcodeFragment : Fragment() {
)
}
view.findViewById<View>(R.id.share).setOnClickListener {
findViewById<View>(R.id.share).setOnClickListener {
it.context?.apply {
pickFileType(R.string.share_as) { fileType ->
shareAs(fileType)
@@ -136,12 +120,10 @@ class BarcodeFragment : Fragment() {
}
}
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
imageView.doOnApplyWindowInsets { _, insets ->
imageView.insets.set(insets)
}
return view
}
private fun Bundle.toBarcode(): Barcode<*> {
@@ -172,7 +154,7 @@ class BarcodeFragment : Fragment() {
content,
barcodeFormat,
getInt(EC_LEVEL),
getInt(MARGIN),
getBoolean(ADD_QUIET_ZONE),
colors
)
}
@@ -182,9 +164,9 @@ class BarcodeFragment : Fragment() {
super.onResume()
if (prefs.brightenScreen) {
// Post to make sure brightenScreenItem is initialized.
view?.post {
contentView?.postDelayed({
brightenScreen()
}
}, 500)
}
}
@@ -198,8 +180,8 @@ class BarcodeFragment : Fragment() {
}
}
override fun onDestroyView() {
super.onDestroyView()
override fun onDestroy() {
super.onDestroy()
parentJob.cancel()
}
@@ -216,8 +198,8 @@ class BarcodeFragment : Fragment() {
barcode.bitmap(),
barcode.format
)
addToHistoryItem.isVisible = false
context.toast(R.string.added_to_history)
addToHistoryItem?.isVisible = false
toast(R.string.added_to_history)
true
}
@@ -232,7 +214,7 @@ class BarcodeFragment : Fragment() {
}
R.id.export_to_file -> {
context.pickFileType(R.string.export_as) { fileType ->
pickFileType(R.string.export_as) { fileType ->
when (fileType) {
FileType.PNG,
FileType.JPG -> askForSize { size ->
@@ -261,10 +243,8 @@ class BarcodeFragment : Fragment() {
}
private fun copyToClipboard(text: String, isSensitive: Boolean = false) {
activity?.apply {
copyToClipboard(text, isSensitive)
toast(R.string.copied_to_clipboard)
}
(this as android.content.Context).copyToClipboard(text, isSensitive)
toast(R.string.copied_to_clipboard)
}
private fun deeplinkToCopy() = createEncodeDeeplink(
@@ -277,7 +257,7 @@ class BarcodeFragment : Fragment() {
action: (FileType) -> Unit
) {
val fileTypes = FileType.entries.toTypedArray()
AlertDialog.Builder(this)
MaterialAlertDialogBuilder(this)
.setTitle(title)
.setItems(fileTypes.map { it.name }.toTypedArray()) { _, which ->
action(fileTypes[which])
@@ -288,21 +268,20 @@ class BarcodeFragment : Fragment() {
// Dialogs do not have a parent view.
@SuppressLint("InflateParams")
private fun askForFileNameAndSave(fileType: FileType, bitmapSize: Int) {
val ac = activity ?: return
// Write permission is only required before Android Q.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
!ac.hasWritePermission {
!hasWritePermission {
askForFileNameAndSave(fileType, bitmapSize)
}
) {
return
}
val view = ac.layoutInflater.inflate(R.layout.dialog_save_file, null)
val view = layoutInflater.inflate(R.layout.dialog_save_file, null)
val editText = view.findViewById<EditText>(R.id.file_name)
editText.setText(
encodeFileName("${barcode.format}_${barcode.content}")
)
AlertDialog.Builder(ac)
MaterialAlertDialogBuilder(this)
.setView(view)
.setPositiveButton(android.R.string.ok) { _, _ ->
val fileName = editText.text.toString()
@@ -344,12 +323,11 @@ class BarcodeFragment : Fragment() {
// Dialogs do not have a parent view.
@SuppressLint("InflateParams")
private fun askForSize(write: (size: Int) -> Unit) {
val ac = activity ?: return
val view = ac.layoutInflater.inflate(R.layout.dialog_size, null)
val view = layoutInflater.inflate(R.layout.dialog_size, null)
val sizeView = view.findViewById<TextView>(R.id.size_display)
val sizeBarView = view.findViewById<SeekBar>(R.id.size_bar)
sizeBarView.initSizeBar(sizeView)
AlertDialog.Builder(ac)
MaterialAlertDialogBuilder(this)
.setView(view)
.setPositiveButton(android.R.string.ok) { _, _ ->
write(getSize(sizeBarView.progress))
@@ -392,13 +370,12 @@ class BarcodeFragment : Fragment() {
mimeType: String,
write: (outputStream: OutputStream) -> Unit
) {
val ac = activity ?: return
scope.launch(Dispatchers.IO) {
val message = ac.writeExternalFile(
val message = writeExternalFile(
fileName, mimeType, write
).toSaveResult()
withContext(Dispatchers.Main) {
ac.toast(message)
toast(message)
}
}
}
@@ -449,21 +426,19 @@ class BarcodeFragment : Fragment() {
}
private fun brightenScreen() {
activity?.let {
if (!isAdded) {
return
}
currentBrightness = it.getScreenBrightness()
it.setScreenBrightness(1f)
brightenScreenItem.isChecked = true
if (isFinishing || isDestroyed) {
return
}
currentBrightness = getScreenBrightness()
setScreenBrightness(1f)
brightenScreenItem?.isChecked = true
}
private fun restoreScreenBrightness() {
if (currentBrightness > -1f && isAdded) {
activity?.setScreenBrightness(currentBrightness)
if (currentBrightness > -1f) {
setScreenBrightness(currentBrightness)
currentBrightness = -1f
brightenScreenItem.isChecked = false
brightenScreenItem?.isChecked = false
}
}
@@ -475,22 +450,23 @@ class BarcodeFragment : Fragment() {
private const val CONTENT_RAW = "content_raw"
private const val FORMAT = "format"
private const val EC_LEVEL = "ec_level"
private const val MARGIN = "margin"
private const val ADD_QUIET_ZONE = "add_quiet_zone"
private const val COLORS = "colors"
private const val MIME_PNG = "image/png"
private const val MIME_JPG = "image/jpeg"
private const val MIME_SVG = "image/svg+xmg"
private const val MIME_TXT = "text/plain"
fun newInstance(
fun newIntent(
context: Context,
barcode: Barcode<*>,
colors: Int = 0
): Fragment {
): Intent {
val args = Bundle()
when (barcode) {
is ContentBarcode -> args.putEcLevelMargin(
is ContentBarcode -> args.putEcLevelQuietZone(
barcode.ecLevel,
barcode.margin
barcode.addQuietZone
)
is BitMatrixBarcode -> {
@@ -498,32 +474,31 @@ class BarcodeFragment : Fragment() {
args.putInt(BIT_MATRIX_HEIGHT, barcode.bitMatrix.height)
args.putByteArray(BIT_MATRIX_DATA, barcode.bitMatrix.data)
}
else -> throw IllegalArgumentException("unknown barcode")
}
args.putContentFormatColors(
barcode.content,
barcode.format,
colors
)
val fragment = BarcodeFragment()
fragment.arguments = args
return fragment
return Intent(context, BarcodeActivity::class.java).apply {
putExtras(args)
}
}
fun <T> newInstance(
fun <T> newIntent(
context: Context,
content: T,
format: BarcodeFormat,
ecLevel: Int = -1,
margin: Int = -1,
addQuietZone: Boolean = false,
colors: Int = 0
): Fragment {
): Intent {
val args = Bundle()
args.putContentFormatColors(content, format, colors)
args.putEcLevelMargin(ecLevel, margin)
val fragment = BarcodeFragment()
fragment.arguments = args
return fragment
args.putEcLevelQuietZone(ecLevel, addQuietZone)
return Intent(context, BarcodeActivity::class.java).apply {
putExtras(args)
}
}
private fun <T> Bundle.putContentFormatColors(
@@ -550,12 +525,12 @@ class BarcodeFragment : Fragment() {
putInt(COLORS, colors)
}
private fun Bundle.putEcLevelMargin(
private fun Bundle.putEcLevelQuietZone(
ecLevel: Int,
margin: Int
addQuietZone: Boolean
) {
putInt(EC_LEVEL, ecLevel)
putInt(MARGIN, margin)
putBoolean(ADD_QUIET_ZONE, addQuietZone)
}
}
}
@@ -610,7 +585,10 @@ private fun readAndAddToHistory(
formats = setOf(format)
)
)?.first {
db.insertScan(it.toScan())
val scan = it.toScan()
if (!prefs.shouldIgnoreHistoryContent(scan.text, scan.format.name)) {
db.insertScan(scan)
}
true
}
}
File diff suppressed because it is too large Load Diff
@@ -1,14 +1,14 @@
package de.markusfisch.android.binaryeye.fragment
package de.markusfisch.android.binaryeye.activity
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.DocumentsContract
import android.support.design.widget.FloatingActionButton
import android.support.v4.app.Fragment
import android.text.Editable
import android.text.Html
import android.text.Spannable
@@ -16,37 +16,43 @@ import android.text.SpannableString
import android.text.TextWatcher
import android.text.method.LinkMovementMethod
import android.text.style.TypefaceSpan
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TableLayout
import android.widget.TableRow
import android.widget.TextView
import androidx.core.net.toUri
import androidx.core.widget.TextViewCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.FloatingActionButton
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.ActionRegistry
import de.markusfisch.android.binaryeye.actions.mail.MatMsg
import de.markusfisch.android.binaryeye.actions.mail.MatMsgAction
import de.markusfisch.android.binaryeye.actions.otpauth.OtpauthAction
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
import de.markusfisch.android.binaryeye.actions.vtype.vcard.VCardAction
import de.markusfisch.android.binaryeye.actions.vtype.vevent.VEventAction
import de.markusfisch.android.binaryeye.actions.web.WebAction
import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
import de.markusfisch.android.binaryeye.actions.wifi.WifiConnector
import de.markusfisch.android.binaryeye.activity.MainActivity
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
import de.markusfisch.android.binaryeye.app.addFragment
import de.markusfisch.android.binaryeye.adapter.toFormatDescriptionResId
import de.markusfisch.android.binaryeye.app.db
import de.markusfisch.android.binaryeye.app.hasLocationPermission
import de.markusfisch.android.binaryeye.app.hasWritePermission
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.content.ContentBarcode
import de.markusfisch.android.binaryeye.content.EpcQrParser
import de.markusfisch.android.binaryeye.content.IdlParser
import de.markusfisch.android.binaryeye.content.SealParser
import de.markusfisch.android.binaryeye.content.copyToClipboard
import de.markusfisch.android.binaryeye.content.epcQrToRes
import de.markusfisch.android.binaryeye.content.idlToRes
import de.markusfisch.android.binaryeye.content.shareAsFile
import de.markusfisch.android.binaryeye.content.shareText
@@ -54,6 +60,8 @@ import de.markusfisch.android.binaryeye.content.toBarcode
import de.markusfisch.android.binaryeye.content.toErrorCorrectionInt
import de.markusfisch.android.binaryeye.content.wipeLastShareFile
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.database.toBundle
import de.markusfisch.android.binaryeye.database.toScan
import de.markusfisch.android.binaryeye.io.askForFileName
import de.markusfisch.android.binaryeye.io.toSaveResult
import de.markusfisch.android.binaryeye.io.writeExternalFile
@@ -67,16 +75,24 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONException
import org.json.JSONObject
import java.io.File
import java.security.MessageDigest
import kotlin.math.roundToInt
class DecodeFragment : Fragment() {
class DecodeActivity : AbstractBaseActivity() {
private lateinit var contentView: EditText
private lateinit var formatView: TextView
private lateinit var dataView: TableLayout
private lateinit var contentHeadlineView: TextView
private lateinit var characterCountView: TextView
private lateinit var dataHeadlineView: TextView
private lateinit var formatHeadlineView: TextView
private lateinit var dataView: LinearLayout
private lateinit var hexHeadlineView: TextView
private lateinit var metaView: TableLayout
private lateinit var hexView: TextView
private lateinit var formatDescriptionView: TextView
private lateinit var labelHeadlineView: TextView
private lateinit var stampView: TextView
private lateinit var recreationView: ImageView
private lateinit var labelView: EditText
@@ -97,6 +113,9 @@ class DecodeFragment : Fragment() {
private var action = ActionRegistry.DEFAULT_ACTION
private var isBinary = false
private var originalBytes: ByteArray = ByteArray(0)
private var lastParsedDataItems: List<Field> = emptyList()
private var lastParsedDataTitleResId = 0
private var showingDimmed = false
private var label: String? = null
private var recreationSize = 0
@@ -105,11 +124,12 @@ class DecodeFragment : Fragment() {
resultCode: Int,
resultData: Intent?
) {
super.onActivityResult(requestCode, resultCode, resultData)
when (requestCode) {
OPEN_DOCUMENT -> {
if (resultCode == Activity.RESULT_OK) {
val uri = resultData?.data ?: return
activity?.openPickedFile(uri)
openPickedFile(uri)
}
}
}
@@ -117,71 +137,79 @@ class DecodeFragment : Fragment() {
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setHasOptionsMenu(true)
}
setScreenContentView(R.layout.activity_decode)
setTitle(R.string.content)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?
): View {
activity?.setTitle(R.string.content)
val view = inflater.inflate(
R.layout.fragment_decode,
container,
false
)
val justScanned = activity?.intent?.hasExtra(
MainActivity.DECODED
) == true
val justScanned = intent.hasExtra(DECODED) || intent.hasExtra(DECODED_SCAN)
closeAutomatically = prefs.closeAutomatically && justScanned
scan = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
arguments?.getParcelable(SCAN, Scan::class.java)
} else {
@Suppress("DEPRECATION")
arguments?.getParcelable(SCAN)
} ?: throw IllegalArgumentException("DecodeFragment needs a Scan")
scan = intent?.extras?.getBundle(SCAN_BUNDLE)?.toScan()
?: throw IllegalArgumentException("DecodeActivity needs a Scan")
isBinary = scan.text.isEmpty()
originalBytes = scan.raw ?: scan.text.toByteArray()
format = scan.format.name
recreationSize = (200f * dp).roundToInt()
contentView = view.findViewById(R.id.content)
formatView = view.findViewById(R.id.format)
dataView = view.findViewById(R.id.data)
metaView = view.findViewById(R.id.meta)
hexView = view.findViewById(R.id.hex)
stampView = view.findViewById(R.id.stamp)
recreationView = view.findViewById(R.id.recreation)
labelView = view.findViewById(R.id.label)
fab = view.findViewById(R.id.open)
contentView = findViewById(R.id.content)
contentHeadlineView = findViewById(R.id.content_headline)
characterCountView = findViewById(R.id.character_count)
dataHeadlineView = findViewById(R.id.data_headline)
formatHeadlineView = findViewById(R.id.format_headline)
dataView = findViewById(R.id.data)
hexHeadlineView = findViewById(R.id.hex_headline)
metaView = findViewById(R.id.meta)
hexView = findViewById(R.id.hex)
formatDescriptionView = findViewById(R.id.format_description)
labelHeadlineView = findViewById(R.id.label_headline)
stampView = findViewById(R.id.stamp)
recreationView = findViewById(R.id.recreation)
labelView = findViewById(R.id.label)
fab = findViewById(R.id.open)
contentHeadlineView.setText(
if (isBinary) {
R.string.section_raw_barcode_content
} else {
R.string.section_raw_barcode_content_editable
}
)
if (prefs.showMetaData) {
val descResId = format.toFormatDescriptionResId()
formatHeadlineView.text = getString(
R.string.section_format_details,
format.prettifyFormatName()
)
formatHeadlineView.visibility = View.VISIBLE
if (descResId != 0) {
formatDescriptionView.setText(descResId)
} else {
formatDescriptionView.visibility = View.GONE
}
metaView.fillMetaView(scan)
} else {
formatHeadlineView.visibility = View.GONE
formatDescriptionView.visibility = View.GONE
metaView.visibility = View.GONE
}
if (scan.id > 0) {
labelHeadlineView.visibility = View.VISIBLE
scan.label?.let {
labelView.setText(it)
label = it
}
} else {
labelHeadlineView.visibility = View.GONE
labelView.visibility = View.GONE
}
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
view.findViewById<View>(R.id.scroll_view).setPaddingFromWindowInsets()
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
findViewById<View>(R.id.scroll_view).setPaddingFromWindowInsets()
initContentAndFab(justScanned)
updateViewsAndFab(scan.text, originalBytes)
return view
}
override fun onPause() {
@@ -194,7 +222,7 @@ class DecodeFragment : Fragment() {
label = newLabel
}
}
if (action.fired) {
if (action.executed) {
maybeBackOrFinish()
}
}
@@ -272,15 +300,13 @@ class DecodeFragment : Fragment() {
}
private fun updateViews(text: String, bytes: ByteArray) {
formatView.text = resources.getQuantityString(
R.plurals.barcode_info,
dataView.fillDataView(text, bytes, text != scan.text)
stampView.setTrackingLink(bytes, format)
characterCountView.text = resources.getQuantityString(
R.plurals.character_count,
bytes.size,
format.prettifyFormatName(),
bytes.size
)
hexView.showIf(prefs.showHexDump) { v ->
v.text = hexDump(bytes)
}
recreationView.showIf(prefs.showRecreation) { v ->
val unmodified: Boolean
val content = if (isBinary) {
@@ -304,7 +330,8 @@ class DecodeFragment : Fragment() {
ContentBarcode(
content,
scan.format,
scan.errorCorrectionLevel.toErrorCorrectionInt()
scan.errorCorrectionLevel.toErrorCorrectionInt(),
addQuietZone = true
)
}
try {
@@ -312,8 +339,8 @@ class DecodeFragment : Fragment() {
v.setOnClickListener {
v.context.hideSoftKeyboard(contentView)
v.post {
fragmentManager?.addFragment(
BarcodeFragment.newInstance(barcode)
startActivity(
BarcodeActivity.newIntent(this, barcode)
)
}
}
@@ -321,8 +348,13 @@ class DecodeFragment : Fragment() {
clearRecreation()
}
}
dataView.fillDataView(text, bytes)
stampView.setTrackingLink(bytes, format)
hexView.showIf(prefs.showHexDump) { v ->
hexHeadlineView.visibility = View.VISIBLE
v.text = hexDump(bytes)
}
if (!prefs.showHexDump) {
hexHeadlineView.visibility = View.GONE
}
}
private fun clearRecreation() {
@@ -332,93 +364,170 @@ class DecodeFragment : Fragment() {
}
}
private fun TableLayout.fillDataView(text: String, bytes: ByteArray) {
val items = LinkedHashMap<Any, CharSequence?>()
private fun LinearLayout.fillDataView(
text: String,
bytes: ByteArray,
isEditing: Boolean
) {
val items = mutableListOf<Field>()
when (prefs.showChecksum) {
"CRC4" -> items[R.string.crc4] = String.format("%X", crc4(bytes))
"MD5" -> items[R.string.md5] = bytes.md5().toHexString().fold()
"SHA1" -> items[R.string.sha1] = bytes.sha1().toHexString().fold()
"SHA256" -> items[R.string.sha256] =
bytes.sha256().toHexString().fold()
"CRC4" -> items.add(Field(R.string.crc4, String.format("%X", crc4(bytes))))
"MD5" -> items.add(Field(R.string.md5, bytes.md5().toHexString().fold()))
"SHA1" -> items.add(Field(R.string.sha1, bytes.sha1().toHexString().fold()))
"SHA256" -> items.add(Field(R.string.sha256, bytes.sha256().toHexString().fold()))
}
val count = items.count()
val parsedDataTitleResId = parseData(items, text, bytes)
if (items.count() > count) {
lastParsedDataItems = items
lastParsedDataTitleResId = parsedDataTitleResId
.takeIf { it != 0 } ?: R.string.section_parsed_data
fillDataItems(items, false, lastParsedDataTitleResId)
return
}
if (isEditing && lastParsedDataItems.isNotEmpty()) {
if (!showingDimmed) {
fillDataItems(lastParsedDataItems, true, lastParsedDataTitleResId)
}
return
}
val titleResId = if (items.isNotEmpty()) {
R.string.section_parsed_data
} else {
0
}
fillDataItems(items, false, titleResId)
}
else -> Unit
private fun parseData(
items: MutableList<Field>,
text: String,
bytes: ByteArray
): Int {
val ctx = this
try {
items.add(Field(R.string.formatted_json, JSONObject(text).toString(2)))
return R.string.parsed_type_json
} catch (_: JSONException) {
// Ignore
}
IdlParser.parse(String(bytes))?.let {
items.putAll(mapOf("IIN" to it.iin))
items.putAll(
it.elements.entries.associate { (id, value) ->
context.idlToRes(id) to value
}
)
items.add(Field("IIN", it.iin))
it.elements.forEach { (id, value) ->
items.add(Field(ctx.idlToRes(id), value))
}
return R.string.parsed_type_international_driver_license
}
SealParser.parse(ctx, bytes)?.let { sealFields ->
sealFields.forEach { vf ->
items.add(Field(vf.name, vf.value))
}
return R.string.parsed_type_digital_seal
}
EpcQrParser.parse(text)?.let {
items.addAll(it.map { (id, value) ->
Field(ctx.epcQrToRes(id), value)
})
return R.string.parsed_type_sepa_epc_qr
}
when (action) {
is MatMsgAction -> items.putAll(
MatMsg(text).run {
mapOf(
R.string.email_to to to,
R.string.email_subject to sub,
R.string.email_body to body
)
}
)
is MatMsgAction -> MatMsg(text).run {
items.add(Field(R.string.email_to, to))
items.add(Field(R.string.email_subject, sub))
items.add(Field(R.string.email_body, body))
return R.string.parsed_type_email
}
is VCardAction,
is VEventAction -> VTypeParser.parseMap(text).let { vData ->
items.putAll(
vData.map { item ->
item.key to item.value.joinToString("\n") {
it.value
}
}.toMap()
)
is VEventAction -> VTypeParser.parseMap(text).let { parsedMap ->
parsedMap.forEach { item ->
items.add(Field(item.key, item.value.joinToString("\n") { it.value }))
}
return if (action is VCardAction) {
R.string.parsed_type_contact_card
} else {
R.string.parsed_type_calendar_event
}
}
is WebAction -> try {
items.putAll(
Uri.parse(text).run {
mapOf(
R.string.scheme to scheme,
R.string.host to host,
R.string.query to query
)
text.toUri().run {
items.add(Field(R.string.scheme, scheme))
items.add(Field(R.string.host, host))
items.add(Field(R.string.query, query))
}
return R.string.parsed_type_url
} catch (_: Exception) {
// Ignore
}
is OtpauthAction -> try {
text.toUri().run {
val label = pathSegments.firstOrNull()?.let {
Uri.decode(it)
} ?: ""
val colonIdx = label.indexOf(':')
val issuerFromLabel = if (colonIdx >= 0) {
label.substring(0, colonIdx)
} else {
null
}
)
val account = if (colonIdx >= 0) {
label.substring(colonIdx + 1).trim()
} else {
label
}
val issuer = getQueryParameter("issuer") ?: issuerFromLabel
items.add(Field(R.string.entry_type, host?.uppercase()))
items.add(Field(R.string.otp_account, account))
items.add(Field(R.string.otp_issuer, issuer))
items.add(Field(R.string.otp_algorithm, getQueryParameter("algorithm")))
items.add(Field(R.string.otp_digits, getQueryParameter("digits")))
if (host == "totp") {
items.add(Field(R.string.otp_period, getQueryParameter("period")))
} else {
items.add(Field(R.string.otp_counter, getQueryParameter("counter")))
}
}
return R.string.parsed_type_otp
} catch (_: Exception) {
// Ignore
}
is WifiAction -> WifiConnector.parseMap(text)?.let { wifiData ->
items.putAll(
linkedMapOf(
R.string.entry_type to getString(R.string.wifi_network),
R.string.wifi_ssid to wifiData["S"],
R.string.wifi_password to wifiData["P"],
R.string.wifi_type to wifiData["T"],
R.string.wifi_hidden to wifiData["H"],
R.string.wifi_eap to wifiData["E"],
R.string.wifi_identity to wifiData["I"],
R.string.wifi_anonymous_identity to wifiData["A"],
R.string.wifi_phase2 to wifiData["PH2"]
)
)
items.add(Field(R.string.entry_type, getString(R.string.wifi_network)))
items.add(Field(R.string.wifi_ssid, wifiData["S"]))
items.add(Field(R.string.wifi_password, wifiData["P"]))
items.add(Field(R.string.wifi_type, wifiData["T"]))
items.add(Field(R.string.wifi_hidden, wifiData["H"]))
items.add(Field(R.string.wifi_eap, wifiData["E"]))
items.add(Field(R.string.wifi_identity, wifiData["I"]))
items.add(Field(R.string.wifi_anonymous_identity, wifiData["A"]))
items.add(Field(R.string.wifi_phase2, wifiData["PH2"]))
return R.string.parsed_type_wifi_network
}
}
fill(items)
return 0
}
private fun TableLayout.fillMetaView(scan: Scan) {
val items = linkedMapOf<Any, CharSequence?>(
R.string.error_correction_level to scan.errorCorrectionLevel,
R.string.sequence_size to scan.sequenceSize.positiveToString(),
R.string.sequence_index to scan.sequenceIndex.positiveToString(),
R.string.sequence_id to scan.sequenceId,
R.string.gtin_country to scan.country,
R.string.gtin_add_on to scan.addOn,
R.string.gtin_price to scan.price,
R.string.gtin_issue_number to scan.issueNumber,
val items = mutableListOf(
Field(R.string.error_correction_level, scan.errorCorrectionLevel),
Field(R.string.sequence_size, scan.sequenceSize.positiveToString()),
Field(R.string.sequence_index, scan.sequenceIndex.positiveToString()),
Field(R.string.sequence_id, scan.sequenceId),
Field(R.string.gtin_country, scan.country),
Field(R.string.gtin_add_on, scan.addOn),
Field(R.string.gtin_price, scan.price),
Field(R.string.gtin_issue_number, scan.issueNumber),
)
if (!scan.version.isNullOrBlank()) {
val versionString = if (scan.format.name == "QR_CODE") {
val versionString = if (scan.format == ZxingCpp.BarcodeFormat.QRCode) {
getString(
R.string.qr_version_and_modules,
scan.version,
@@ -427,27 +536,17 @@ class DecodeFragment : Fragment() {
} else {
scan.version
}
items.putAll(
linkedMapOf(
R.string.barcode_version_number to versionString
)
)
items.add(Field(R.string.barcode_version_number, versionString))
}
if (scan.format == ZxingCpp.BarcodeFormat.QR_CODE &&
if (scan.format == ZxingCpp.BarcodeFormat.QRCode &&
scan.dataMask > -1
) {
items.putAll(
linkedMapOf(
R.string.qr_data_mask to scan.dataMask.toString()
)
)
items.add(Field(R.string.qr_data_mask, scan.dataMask.toString()))
}
fill(items)
fillTable(items)
}
private fun TableLayout.fill(
items: LinkedHashMap<Any, CharSequence?>
) {
private fun TableLayout.fillTable(items: List<Field>) {
removeAllViews()
visibility = if (items.isEmpty()) View.GONE else {
val ctx = context
@@ -456,11 +555,12 @@ class DecodeFragment : Fragment() {
val text = item.value
if (!text.isNullOrBlank()) {
val tr = TableRow(ctx)
val keyView = TextView(ctx)
when (val key = item.key) {
is Int -> keyView.setText(key)
is String -> keyView.text = key
else -> keyView.text = key.toString()
val keyView = TextView(ctx).apply {
when (val key = item.key) {
is Int -> setText(key)
is String -> this.text = key
else -> this.text = key.toString()
}
}
val valueView = TextView(ctx).apply {
setPadding(spaceBetween, 0, 0, 0)
@@ -478,9 +578,93 @@ class DecodeFragment : Fragment() {
}
}
private fun LinearLayout.fillDataItems(
items: List<Field>,
dimmed: Boolean,
titleResId: Int
) {
showingDimmed = dimmed
removeAllViews()
if (items.isEmpty()) {
dataHeadlineView.visibility = View.GONE
visibility = View.GONE
return
}
if (titleResId != 0) {
dataHeadlineView.setText(titleResId)
dataHeadlineView.visibility = View.VISIBLE
} else {
dataHeadlineView.visibility = View.GONE
}
val ctx = context
val spaceBetween = (16f * dp).roundToInt()
for (item in items) {
val text = item.value
if (text.isNullOrBlank()) {
continue
}
val rowView = LinearLayout(ctx).apply {
orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
bottomMargin = spaceBetween
}
setBackgroundResource(R.drawable.list_selector)
setOnClickListener {
copyToClipboard(text.toString())
}
}
val keyView = TextView(ctx).apply {
TextViewCompat.setTextAppearance(
this,
R.style.SecondaryText
)
when (val key = item.key) {
is Int -> setText(key)
is String -> this.text = key
else -> this.text = key.toString()
}
}
val valueView = TextView(ctx).apply {
TextViewCompat.setTextAppearance(
this,
android.R.style.TextAppearance_Medium
)
setTextColor(
ColorStateList.valueOf(
ctx.obtainStyledAttributes(
intArrayOf(android.R.attr.textColorPrimary)
).run {
try {
getColor(0, currentTextColor)
} finally {
recycle()
}
}
)
)
this.text = text
}
val alpha = if (dimmed) 0.5f else 1f
keyView.alpha = alpha
valueView.alpha = alpha
rowView.addView(keyView)
rowView.addView(valueView)
addView(rowView)
}
visibility = View.VISIBLE
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.fragment_decode, menu)
if (id > 0L) {
if (scan.id > 0L) {
menu.findItem(R.id.remove).isVisible = true
}
if (action is WifiAction) {
@@ -509,29 +693,26 @@ class DecodeFragment : Fragment() {
}
R.id.share -> {
context?.apply {
shareText(textOrHex())
maybeBackOrFinish()
}
shareText(textOrHex())
maybeBackOrFinish()
true
}
R.id.share_file -> {
context?.apply {
val (data, name) = if (isBinary) {
Pair(originalBytes, "barcode_content.bin")
} else {
Pair(content, "barcode_content.txt")
}
shareAsFile(data, name)
maybeBackOrFinish()
val (data, name) = if (isBinary) {
Pair(originalBytes, "barcode_content.bin")
} else {
Pair(content, "barcode_content.txt")
}
shareAsFile(data, name)
maybeBackOrFinish()
true
}
R.id.create -> {
fragmentManager?.addFragment(
EncodeFragment.newInstance(
startActivity(
EncodeActivity.newIntent(
this,
if (isBinary) originalBytes else content,
format
)
@@ -540,8 +721,7 @@ class DecodeFragment : Fragment() {
}
R.id.remove -> {
db.removeScan(scan.id)
backOrFinish()
askToRemoveScan(scan.id)
true
}
@@ -549,6 +729,18 @@ class DecodeFragment : Fragment() {
}
}
private fun askToRemoveScan(id: Long) {
MaterialAlertDialogBuilder(this)
.setMessage(R.string.really_remove_scan)
.setPositiveButton(android.R.string.ok) { _, _ ->
db.removeScan(id)
backOrFinish()
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
}
.show()
}
private fun textOrHex() = if (isBinary) {
originalBytes.toHexString()
} else {
@@ -570,12 +762,10 @@ class DecodeFragment : Fragment() {
}
private fun copyToClipboard(text: String, isSensitive: Boolean = false) {
activity?.apply {
copyToClipboard(text, isSensitive)
// There's a clipboard popup from Android 13 on.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
toast(R.string.copied_to_clipboard)
}
(this as android.content.Context).copyToClipboard(text, isSensitive)
// There's a clipboard popup from Android 13 on.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
toast(R.string.copied_to_clipboard)
}
}
@@ -586,27 +776,21 @@ class DecodeFragment : Fragment() {
}
private fun backOrFinish() {
val fm = fragmentManager
if (fm != null && fm.backStackEntryCount > 0) {
fm.popBackStack()
} else {
activity?.finish()
}
finish()
}
private fun executeAction(str: String) {
val ac = activity ?: return
if (str.isEmpty() || openLocalDocument(str)) {
return
}
if (action is WifiAction &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
!ac.hasLocationPermission { executeAction(str) }
!hasLocationPermission { executeAction(str) }
) {
return
}
scope.launch {
action.execute(ac, str.toByteArray())
action.execute(this@DecodeActivity, str.toByteArray())
}
}
@@ -642,44 +826,48 @@ class DecodeFragment : Fragment() {
}
private fun askForFileNameAndSave(raw: ByteArray) {
val ac = activity ?: return
// Write permission is only required before Android Q.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
!ac.hasWritePermission { askForFileNameAndSave(raw) }
!hasWritePermission { askForFileNameAndSave(raw) }
) {
return
}
scope.launch(Dispatchers.Main) {
val name = ac.askForFileName() ?: return@launch
val name = askForFileName() ?: return@launch
withContext(Dispatchers.IO) {
val message = ac.writeExternalFile(
val message = writeExternalFile(
name,
"application/octet-stream"
) {
it.write(raw)
}.toSaveResult()
withContext(Dispatchers.Main) {
ac.toast(message)
toast(message)
}
}
}
}
companion object {
private const val SCAN = "scan"
const val DECODED_SCAN = "decoded_scan"
const val DECODED = "decoded"
private const val SCAN_BUNDLE = "scan_bundle"
private const val OPEN_DOCUMENT = 1
private const val SCHEME_FILE = "file://"
fun newInstance(scan: Scan): Fragment {
fun newIntent(context: Context, scan: Scan): Intent {
val args = Bundle()
args.putParcelable(SCAN, scan)
val fragment = DecodeFragment()
fragment.arguments = args
return fragment
args.putBundle(SCAN_BUNDLE, scan.toBundle())
return Intent(context, DecodeActivity::class.java).apply {
putExtra(DECODED_SCAN, true)
putExtras(args)
}
}
}
}
private data class Field(val key: Any, val value: CharSequence?)
private inline fun <T : View> T.showIf(
visible: Boolean,
block: (T) -> Unit
@@ -741,6 +929,7 @@ private fun Int.positiveToString() = if (this > -1) this.toString() else ""
private fun TextView.setTrackingLink(bytes: ByteArray, format: String) {
val trackingLink = generateDpTrackingLink(bytes, format)
if (trackingLink != null) {
visibility = View.VISIBLE
text = trackingLink.fromHtml()
isClickable = true
movementMethod = LinkMovementMethod.getInstance()
@@ -753,7 +942,7 @@ private fun generateDpTrackingLink(bytes: ByteArray, format: String): String? {
// Check for Deutsche Post Matrixcode stamp.
var isStamp = false
var rawData = bytes
if (format == "DATA_MATRIX" &&
if (format == ZxingCpp.BarcodeFormat.DataMatrix.name &&
bytes.toString(Charsets.ISO_8859_1).startsWith("DEA5")
) {
if (bytes.size == 47) {
@@ -1,28 +1,26 @@
package de.markusfisch.android.binaryeye.fragment
package de.markusfisch.android.binaryeye.activity
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.widget.SwitchCompat
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.CheckBox
import android.widget.EditText
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.widget.SwitchCompat
import androidx.core.net.toUri
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
import de.markusfisch.android.binaryeye.app.addFragment
import de.markusfisch.android.binaryeye.adapter.toFormatDescriptionResId
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.app.setFragment
import de.markusfisch.android.binaryeye.net.isEncodeDeeplink
import de.markusfisch.android.binaryeye.text.unescape
import de.markusfisch.android.binaryeye.view.hideSoftKeyboard
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
@@ -31,8 +29,9 @@ import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
import java.io.InputStream
import kotlin.math.min
class EncodeFragment : Fragment() {
class EncodeActivity : AbstractBaseActivity() {
private lateinit var formatView: Spinner
private lateinit var formatDescriptionView: TextView
private lateinit var ecLabel: TextView
private lateinit var ecSpinner: Spinner
private lateinit var colorsLabel: TextView
@@ -41,18 +40,33 @@ class EncodeFragment : Fragment() {
private lateinit var contentView: EditText
private lateinit var unescapeCheckBox: CheckBox
private val formats = arrayListOf(
BarcodeFormat.AZTEC,
BarcodeFormat.CODABAR,
BarcodeFormat.CODE_39,
BarcodeFormat.CODE_128,
BarcodeFormat.DATA_MATRIX,
BarcodeFormat.EAN_8,
BarcodeFormat.EAN_13,
private val writeableFormats = arrayListOf(
BarcodeFormat.Aztec,
BarcodeFormat.Codabar,
BarcodeFormat.Code39Std,
BarcodeFormat.Code39Ext,
BarcodeFormat.Code32,
BarcodeFormat.PZN,
BarcodeFormat.Code93,
BarcodeFormat.Code128,
BarcodeFormat.DataBar,
BarcodeFormat.DataBarOmni,
BarcodeFormat.DataBarStk,
BarcodeFormat.DataBarLtd,
BarcodeFormat.DataBarExp,
BarcodeFormat.DataBarExpStk,
BarcodeFormat.DataMatrix,
BarcodeFormat.DXFilmEdge,
BarcodeFormat.EAN8,
BarcodeFormat.EAN13,
BarcodeFormat.ITF,
BarcodeFormat.PDF_417,
BarcodeFormat.QR_CODE,
BarcodeFormat.UPC_A
BarcodeFormat.MaxiCode,
BarcodeFormat.PDF417,
BarcodeFormat.QRCode,
BarcodeFormat.MicroQRCode,
BarcodeFormat.RMQRCode,
BarcodeFormat.UPCA,
BarcodeFormat.UPCE,
)
private var bytes: ByteArray? = null
@@ -62,16 +76,16 @@ class EncodeFragment : Fragment() {
resultCode: Int,
resultData: Intent?
) {
super.onActivityResult(requestCode, resultCode, resultData)
when (requestCode) {
PICK_FILE_RESULT_CODE -> {
if (resultCode == Activity.RESULT_OK &&
resultData != null &&
resultData.data != null
) {
val ac = activity ?: return
ac.hideSoftKeyboard(contentView)
hideSoftKeyboard(contentView)
val uri = resultData.data ?: return
bytes = ac.contentResolver?.openInputStream(uri)?.use {
bytes = contentResolver?.openInputStream(uri)?.use {
it.readBytesMax(4096)
} ?: return
setEncodeByteArray()
@@ -82,28 +96,15 @@ class EncodeFragment : Fragment() {
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setHasOptionsMenu(true)
}
setScreenContentView(R.layout.activity_encode)
setTitle(R.string.compose_barcode)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?
): View? {
val ac = activity ?: return null
ac.setTitle(R.string.compose_barcode)
val view = inflater.inflate(
R.layout.fragment_encode,
container,
false
)
formatView = view.findViewById(R.id.format)
formatView = findViewById(R.id.format)
formatDescriptionView = findViewById(R.id.format_description)
val formatAdapter = ArrayAdapter(
ac,
this,
android.R.layout.simple_spinner_item,
formats.map { it.name.prettifyFormatName() }
writeableFormats.map { it.name.prettifyFormatName() }
)
formatAdapter.setDropDownViewResource(
android.R.layout.simple_spinner_dropdown_item
@@ -116,11 +117,20 @@ class EncodeFragment : Fragment() {
position: Int,
id: Long
) {
val format = formats[position]
val format = writeableFormats[position]
val descResId = format.name.toFormatDescriptionResId()
if (descResId != 0) {
formatDescriptionView.setText(descResId)
formatDescriptionView.visibility = View.VISIBLE
} else {
formatDescriptionView.visibility = View.GONE
}
val arrayId = when (format) {
BarcodeFormat.AZTEC -> R.array.aztec_error_correction_levels
BarcodeFormat.QR_CODE -> R.array.qr_error_correction_levels
BarcodeFormat.PDF_417 -> R.array.pdf417_error_correction_levels
BarcodeFormat.Aztec -> R.array.aztec_error_correction_levels
BarcodeFormat.QRCode -> R.array.qr_error_correction_levels
BarcodeFormat.MicroQRCode -> R.array.mqr_error_correction_levels
BarcodeFormat.RMQRCode -> R.array.rmqr_error_correction_levels
BarcodeFormat.PDF417 -> R.array.pdf417_error_correction_levels
else -> 0
}
if (arrayId > 0) {
@@ -138,21 +148,13 @@ class EncodeFragment : Fragment() {
format.canBeInverted().setVisibilityFor(
colorsLabel, colorsSpinner
)
when (format) {
BarcodeFormat.AZTEC,
BarcodeFormat.DATA_MATRIX,
BarcodeFormat.QR_CODE,
BarcodeFormat.PDF_417 -> true
else -> false
}.setVisibilityFor(addQuietZoneSwitch)
}
override fun onNothingSelected(parentView: AdapterView<*>?) {}
}
ecLabel = view.findViewById(R.id.error_correction_label)
ecSpinner = view.findViewById(R.id.error_correction_level)
ecLabel = findViewById(R.id.error_correction_label)
ecSpinner = findViewById(R.id.error_correction_level)
ecSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parentView: AdapterView<*>?,
@@ -160,7 +162,7 @@ class EncodeFragment : Fragment() {
position: Int,
id: Long
) {
val format = formats[formatView.selectedItemPosition]
val format = writeableFormats[formatView.selectedItemPosition]
prefs.indexOfLastSelectedEcLevel = format.packEcLevel(
prefs.indexOfLastSelectedEcLevel,
position
@@ -170,55 +172,29 @@ class EncodeFragment : Fragment() {
override fun onNothingSelected(parentView: AdapterView<*>?) {}
}
colorsLabel = view.findViewById(R.id.colors_label)
colorsSpinner = view.findViewById(R.id.colors)
addQuietZoneSwitch = view.findViewById(R.id.add_quiet_zone)
colorsLabel = findViewById(R.id.colors_label)
colorsSpinner = findViewById(R.id.colors)
addQuietZoneSwitch = findViewById(R.id.add_quiet_zone)
addQuietZoneSwitch.isChecked = prefs.addQuietZone
contentView = view.findViewById(R.id.content)
unescapeCheckBox = view.findViewById(R.id.unescape)
contentView = findViewById(R.id.content)
unescapeCheckBox = findViewById(R.id.unescape)
unescapeCheckBox.isChecked = prefs.expandEscapeSequences
var complete = false
val args = arguments
args?.getString(CONTENT_TEXT)?.let {
contentView.setText(it)
complete = it.isNotEmpty()
}
args?.getByteArray(CONTENT_RAW)?.let {
bytes = it
complete = it.isNotEmpty()
setEncodeByteArray()
}
applyIntent(state == null)
val barcodeFormat = args?.getString(FORMAT)
if (barcodeFormat != null) {
formatView.setSelection(
formats.indexOf(barcodeFormat.toFormat())
)
} else if (state == null) {
formatView.post {
formatView.setSelection(prefs.indexOfLastSelectedFormat)
}
}
view.findViewById<View>(R.id.encode).setOnClickListener {
findViewById<View>(R.id.encode).setOnClickListener {
it.context.encode()
}
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
view.findViewById<View>(R.id.scroll_view).setPaddingFromWindowInsets()
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
findViewById<View>(R.id.scroll_view).setPaddingFromWindowInsets()
}
if (complete &&
args?.getBoolean(EXECUTE, false) == true
) {
view.post {
val content = view.context.getContent() ?: return@post
fragmentManager?.setFragment(newEncodeFragment(content))
}
}
return view
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
applyIntent(true)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@@ -251,30 +227,75 @@ class EncodeFragment : Fragment() {
prefs.addQuietZone = addQuietZoneSwitch.isChecked
}
private fun applyIntent(applyDefaultFormat: Boolean) {
val args = Bundle().apply {
intent?.extras?.let { putAll(it) }
getExtrasFromDeeplink()?.let { putAll(it) }
}.takeIf { !it.isEmpty }
val hasTextContent = args?.containsKey(CONTENT_TEXT) == true
val hasRawContent = args?.containsKey(CONTENT_RAW) == true
var complete = bytes?.isNotEmpty() == true ||
contentView.text?.isNotEmpty() == true
if (hasTextContent || hasRawContent) {
bytes = null
contentView.isEnabled = true
contentView.hint = null
unescapeCheckBox.isEnabled = true
}
if (hasTextContent) {
args.getString(CONTENT_TEXT)?.let {
contentView.setText(it)
complete = it.isNotEmpty()
}
}
if (hasRawContent) {
args.getByteArray(CONTENT_RAW)?.let {
bytes = it
complete = it.isNotEmpty()
setEncodeByteArray()
}
}
val barcodeFormat = args?.getString(FORMAT)
if (barcodeFormat != null) {
formatView.setSelection(
writeableFormats.indexOf(barcodeFormat.toFormat())
)
} else if (applyDefaultFormat) {
formatView.post {
formatView.setSelection(prefs.indexOfLastSelectedFormat)
}
}
if (complete &&
args?.getBoolean(EXECUTE, false) == true
) {
contentView.post {
val content = contentView.context.getContent() ?: return@post
startActivity(newBarcodeIntent(content))
finish()
}
}
}
private fun Context.encode() {
hideSoftKeyboard(contentView)
val content = getContent() ?: return
contentView.post {
fragmentManager?.addFragment(newEncodeFragment(content))
startActivity(newBarcodeIntent(content))
}
}
private fun newEncodeFragment(content: Any): Fragment {
val format = formats[formatView.selectedItemPosition]
return BarcodeFragment.newInstance(
private fun newBarcodeIntent(content: Any): Intent {
val format = writeableFormats[formatView.selectedItemPosition]
return BarcodeActivity.newIntent(
this,
content,
format,
format.getErrorCorrectionLevel(ecSpinner.selectedItemPosition),
when (format) {
BarcodeFormat.AZTEC,
BarcodeFormat.DATA_MATRIX,
BarcodeFormat.QR_CODE,
BarcodeFormat.PDF_417 -> if (
addQuietZoneSwitch.isChecked
) 1 else 0
else -> -1
},
addQuietZoneSwitch.isChecked,
if (format.canBeInverted()) {
colorsSpinner.selectedItemPosition
} else 0
@@ -316,6 +337,28 @@ class EncodeFragment : Fragment() {
unescapeCheckBox.isEnabled = false
}
private fun getExtrasFromDeeplink(): Bundle? {
val data = intent?.dataString ?: return null
if (!isEncodeDeeplink(data)) {
return null
}
val uri = data.toUri()
return Bundle().apply {
uri.getQueryParameter("content")?.let {
putString(CONTENT_TEXT, it)
}
uri.getQueryParameter("format")?.let {
putString(FORMAT, it)
}
putBoolean(
EXECUTE,
uri.getQueryParameter("execute").let {
it == "" || it?.toBoolean() == true
}
)
}
}
companion object {
private const val CONTENT_TEXT = "content_text"
private const val CONTENT_RAW = "content_raw"
@@ -323,11 +366,12 @@ class EncodeFragment : Fragment() {
private const val EXECUTE = "execute"
private const val PICK_FILE_RESULT_CODE = 1
fun <T> newInstance(
fun <T> newIntent(
context: Context,
content: T? = null,
format: String? = null,
execute: Boolean = false
): Fragment {
): Intent {
val args = Bundle()
content?.let {
when (content) {
@@ -340,9 +384,9 @@ class EncodeFragment : Fragment() {
args.putBoolean(EXECUTE, execute)
}
format?.let { args.putString(FORMAT, it) }
val fragment = EncodeFragment()
fragment.arguments = args
return fragment
return Intent(context, EncodeActivity::class.java).apply {
putExtras(args)
}
}
}
}
@@ -363,22 +407,26 @@ private fun BarcodeFormat.unpackEcLevel(packed: Int) =
(packed shr ecLevelShift()) and 15
private fun BarcodeFormat.ecLevelShift() = when (this) {
BarcodeFormat.AZTEC -> 0
BarcodeFormat.QR_CODE -> 4
BarcodeFormat.PDF_417 -> 8
BarcodeFormat.Aztec -> 0
BarcodeFormat.QRCode -> 4
BarcodeFormat.MicroQRCode -> 3
BarcodeFormat.RMQRCode -> 2
BarcodeFormat.PDF417 -> 8
else -> throw IllegalArgumentException("$this does not have error levels")
}
private fun BarcodeFormat.canBeInverted() = when (this) {
BarcodeFormat.AZTEC,
BarcodeFormat.DATA_MATRIX,
BarcodeFormat.QR_CODE -> true
BarcodeFormat.Aztec,
BarcodeFormat.DataMatrix,
BarcodeFormat.QRCode,
BarcodeFormat.MicroQRCode,
BarcodeFormat.RMQRCode -> true
else -> false
}
private fun String.toFormat(
default: BarcodeFormat = BarcodeFormat.QR_CODE
default: BarcodeFormat = BarcodeFormat.QRCode
): BarcodeFormat = try {
BarcodeFormat.valueOf(this)
} catch (_: IllegalArgumentException) {
@@ -386,9 +434,12 @@ private fun String.toFormat(
}
private fun BarcodeFormat.getErrorCorrectionLevel(position: Int) = when (this) {
BarcodeFormat.AZTEC -> position
BarcodeFormat.QR_CODE -> (position + 1) * 2
BarcodeFormat.PDF_417 -> position
BarcodeFormat.Aztec -> position
BarcodeFormat.QRCode,
BarcodeFormat.MicroQRCode -> (position + 1) * 2
BarcodeFormat.RMQRCode -> (position + 1) * 4
BarcodeFormat.PDF417 -> position
else -> 0
}.coerceIn(0, 8)
@@ -1,31 +1,27 @@
package de.markusfisch.android.binaryeye.fragment
package de.markusfisch.android.binaryeye.activity
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.SearchManager
import android.content.Context
import android.database.Cursor
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.support.v4.app.ActivityCompat
import android.support.v4.app.Fragment
import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity
import android.support.v7.view.ActionMode
import android.support.v7.widget.SearchView
import android.support.v7.widget.SwitchCompat
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ListView
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.SwitchCompat
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.ScansAdapter
import de.markusfisch.android.binaryeye.app.addFragment
import de.markusfisch.android.binaryeye.app.alertDialog
import de.markusfisch.android.binaryeye.app.db
import de.markusfisch.android.binaryeye.app.hasWritePermission
@@ -38,7 +34,6 @@ import de.markusfisch.android.binaryeye.database.Database
import de.markusfisch.android.binaryeye.database.exportCsv
import de.markusfisch.android.binaryeye.database.exportDatabase
import de.markusfisch.android.binaryeye.database.exportJson
import de.markusfisch.android.binaryeye.database.use
import de.markusfisch.android.binaryeye.io.askForFileName
import de.markusfisch.android.binaryeye.io.toSaveResult
import de.markusfisch.android.binaryeye.view.lockStatusBarColor
@@ -53,7 +48,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class HistoryFragment : Fragment() {
class HistoryActivity : AbstractBaseActivity() {
private lateinit var useHistorySwitch: SwitchCompat
private lateinit var listView: ListView
private lateinit var fab: View
@@ -70,16 +65,15 @@ class HistoryFragment : Fragment() {
R.menu.fragment_history_edit,
menu
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
// SDK35+ doesn't draw a status bar background anymore.
Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM
) {
// SDK35+ doesn't draw a status bar background anymore.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
lockStatusBarColor()
val ac = activity ?: return false
val ac = this@HistoryActivity
@Suppress("DEPRECATION")
ac.window.statusBarColor = ContextCompat.getColor(
ac.window.statusBarColor = MaterialColors.getColor(
ac,
R.color.accent_dark
com.google.android.material.R.attr.colorSecondaryContainer,
Color.TRANSPARENT
)
}
return true
@@ -96,7 +90,7 @@ class HistoryFragment : Fragment() {
mode: ActionMode,
item: MenuItem
): Boolean {
val ac = activity ?: return false
val ac = this@HistoryActivity
return when (item.itemId) {
R.id.copy_scan -> {
scansAdapter?.getSelectedContent("\n")?.let {
@@ -148,29 +142,15 @@ class HistoryFragment : Fragment() {
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setHasOptionsMenu(true)
}
setScreenContentView(R.layout.activity_history)
setTitle(R.string.history)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?
): View? {
val ac = activity ?: return null
ac.setTitle(R.string.history)
val view = inflater.inflate(
R.layout.fragment_history,
container,
false
)
useHistorySwitch = view.findViewById(
useHistorySwitch = findViewById(
R.id.use_history
)
initHistorySwitch(useHistorySwitch)
listView = view.findViewById(R.id.scans)
listView = findViewById(R.id.scans)
listView.setOnItemClickListener { _, v, position, id ->
if (actionMode != null) {
scansAdapter?.select(v, id, position)
@@ -180,8 +160,8 @@ class HistoryFragment : Fragment() {
}
listView.setOnItemLongClickListener { _, v, position, id ->
scansAdapter?.select(v, id, position)
if (actionMode == null && ac is AppCompatActivity) {
actionMode = ac.delegate.startSupportActionMode(
if (actionMode == null) {
actionMode = delegate.startSupportActionMode(
actionModeCallback
)
}
@@ -189,19 +169,15 @@ class HistoryFragment : Fragment() {
}
listView.setOnScrollListener(systemBarListViewScrollListener)
fab = view.findViewById(R.id.share)
fab = findViewById(R.id.share)
fab.setOnClickListener { v ->
v.context.pickListSeparatorAndShare(false)
}
progressView = view.findViewById(R.id.progress_view)
progressView = findViewById(R.id.progress_view)
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
listView.setPaddingFromWindowInsets()
update()
return view
}
override fun onDestroy() {
@@ -211,6 +187,11 @@ class HistoryFragment : Fragment() {
wipeLastShareFile()
}
override fun onResume() {
super.onResume()
update()
}
override fun onPause() {
super.onPause()
listViewState = listView.onSaveInstanceState()
@@ -235,35 +216,31 @@ class HistoryFragment : Fragment() {
private fun initSearchView(item: MenuItem?) {
item ?: return
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
item.isVisible = false
} else {
val ac = activity ?: return
val searchView = item.actionView as SearchView
val searchManager = ac.getSystemService(
Context.SEARCH_SERVICE
) as SearchManager
searchView.setSearchableInfo(
searchManager.getSearchableInfo(ac.componentName)
)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
update(query)
return false
}
val ac = this
val searchView = item.actionView as SearchView
val searchManager = ac.getSystemService(
Context.SEARCH_SERVICE
) as SearchManager
searchView.setSearchableInfo(
searchManager.getSearchableInfo(ac.componentName)
)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
update(query)
return false
}
override fun onQueryTextChange(query: String): Boolean {
update(query)
return false
}
})
}
override fun onQueryTextChange(query: String): Boolean {
update(query)
return false
}
})
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.clear -> {
context.askToRemoveScans()
this.askToRemoveScans()
true
}
@@ -273,7 +250,7 @@ class HistoryFragment : Fragment() {
}
R.id.share_as_file -> {
context.pickListSeparatorAndShare(true)
this.pickListSeparatorAndShare(true)
true
}
@@ -300,13 +277,13 @@ class HistoryFragment : Fragment() {
scope.launch {
val cursor = db.getScans(filter)
withContext(Dispatchers.Main) {
val ac = activity ?: return@withContext
val ac = this@HistoryActivity
val hasScans = cursor.count > 0
if (filter == null) {
if (!hasScans) {
listView.emptyView = useHistorySwitch
}
ActivityCompat.invalidateOptionsMenu(ac)
ac.invalidateOptionsMenu()
}
enableMenuItems(hasScans)
fab.visibility = if (hasScans) {
@@ -317,7 +294,16 @@ class HistoryFragment : Fragment() {
cursor.let { cursor ->
// Close previous cursor.
scansAdapter?.also { it.changeCursor(null) }
scansAdapter = ScansAdapter(ac, cursor)
scansAdapter = ScansAdapter(ac, cursor) { id, pinned ->
if (actionMode == null) {
scope.launch {
db.setPinned(id, pinned)
withContext(Dispatchers.Main) {
update()
}
}
}
}
listView.adapter = scansAdapter
listViewState?.also {
listView.onRestoreInstanceState(it)
@@ -344,7 +330,7 @@ class HistoryFragment : Fragment() {
private fun showScan(id: Long) = db.getScan(id)?.also { scan ->
closeActionMode()
try {
fragmentManager?.addFragment(DecodeFragment.newInstance(scan))
startActivity(DecodeActivity.newIntent(this, scan))
} catch (_: IllegalArgumentException) {
// Ignore, can never happen.
}
@@ -358,7 +344,7 @@ class HistoryFragment : Fragment() {
)
val nameView = view.findViewById<EditText>(R.id.name)
nameView.setText(text)
AlertDialog.Builder(this)
MaterialAlertDialogBuilder(this)
.setTitle(
if (content.isNullOrEmpty()) {
getString(R.string.binary_data)
@@ -377,7 +363,7 @@ class HistoryFragment : Fragment() {
}
private fun Context.askToRemoveScans(ids: List<Long>) {
AlertDialog.Builder(this)
MaterialAlertDialogBuilder(this)
.setMessage(
if (ids.size > 1) {
R.string.really_remove_selected_scans
@@ -399,7 +385,7 @@ class HistoryFragment : Fragment() {
}
private fun Context.askToRemoveScans() {
AlertDialog.Builder(this)
MaterialAlertDialogBuilder(this)
.setMessage(
if (filter == null) {
R.string.really_remove_all_scans
@@ -418,7 +404,7 @@ class HistoryFragment : Fragment() {
private fun askToExportToFile() {
scope.launch {
val ac = activity ?: return@launch
val ac = this@HistoryActivity
progressView.useVisibility {
// Write permission is only required before Android Q.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
@@ -464,7 +450,7 @@ class HistoryFragment : Fragment() {
val separators = resources.getStringArray(
R.array.list_separators_values
)
AlertDialog.Builder(this)
MaterialAlertDialogBuilder(this)
.setTitle(R.string.pick_list_separator)
.setItems(R.array.list_separators_names) { _, which ->
shareScans(separators[which], asFile)
@@ -490,12 +476,12 @@ class HistoryFragment : Fragment() {
text?.let {
withContext(Dispatchers.Main) {
if (asFile) {
context.shareAsFile(
this@HistoryActivity.shareAsFile(
it,
String.format("scans.%s", ext)
)
} else {
context.shareText(it)
this@HistoryActivity.shareText(it)
}
}
}
@@ -0,0 +1,172 @@
package de.markusfisch.android.binaryeye.activity
import android.app.Dialog
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.ListView
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
import de.markusfisch.android.binaryeye.adapter.setupFormatSpinner
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.preference.IgnoreCode
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
import de.markusfisch.android.binaryeye.widget.toast
class IgnoreCodesActivity : AbstractBaseActivity() {
private val patterns = ArrayList<IgnoreCode>()
private lateinit var adapter: PatternsAdapter
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setScreenContentView(R.layout.activity_ignore_codes)
setTitle(R.string.ignore_codes)
patterns.addAll(prefs.ignoreCodes)
val listView = findViewById<ListView>(R.id.actions)
listView.emptyView = findViewById(R.id.no_actions)
findViewById<TextView>(R.id.no_actions).setText(
R.string.ignore_codes_empty
)
adapter = PatternsAdapter(patterns)
listView.adapter = adapter
listView.setOnScrollListener(systemBarListViewScrollListener)
listView.setOnItemClickListener { _, _, position, _ ->
editPattern(position)
}
listView.setOnItemLongClickListener { _, _, position, _ ->
confirmRemovePattern(position)
true
}
findViewById<View>(R.id.add).apply {
contentDescription = getString(R.string.ignore_code_add)
setOnClickListener {
editPattern(null)
}
}
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
listView.setPaddingFromWindowInsets()
}
private fun editPattern(position: Int?) {
val pattern = position?.let { patterns[it] }
val view = layoutInflater.inflate(R.layout.dialog_ignore_code, null)
val regexView = view.findViewById<EditText>(R.id.regex)
val formatView = view.findViewById<Spinner>(R.id.format)
val values = setupFormatSpinner(formatView)
if (pattern != null) {
regexView.setText(pattern.pattern)
regexView.setSelection(pattern.pattern.length)
formatView.setSelection(
values.indexOf(pattern.format ?: "").coerceAtLeast(0)
)
} else {
formatView.setSelection(0)
}
val dialog = MaterialAlertDialogBuilder(this)
.setTitle(
if (pattern == null) {
R.string.ignore_code_add
} else {
R.string.ignore_code_edit
}
)
.setView(view)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.create()
dialog.setOnShowListener {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
addOrUpdatePattern(position, regexView, formatView, values, dialog)
}
}
dialog.show()
}
private fun addOrUpdatePattern(
position: Int?,
regexView: EditText,
formatView: Spinner,
values: List<String>,
dialog: Dialog,
) {
val pattern = regexView.text.toString().trim()
if (pattern.isEmpty()) {
regexView.error = getString(R.string.ignore_code_regex_required)
return
}
try {
Regex(pattern)
} catch (e: Exception) {
val message = e.message ?: getString(
R.string.ignore_code_regex_invalid
)
regexView.error = message
toast(message)
return
}
val item = IgnoreCode(
pattern = pattern,
format = values[formatView.selectedItemPosition].ifEmpty { null }
)
if (position == null) {
patterns.add(item)
} else {
patterns[position] = item
}
prefs.setIgnoreCodes(patterns)
adapter.notifyDataSetChanged()
dialog.dismiss()
}
private fun confirmRemovePattern(position: Int) {
MaterialAlertDialogBuilder(this)
.setMessage(R.string.ignore_code_remove_confirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
patterns.removeAt(position)
prefs.setIgnoreCodes(patterns)
adapter.notifyDataSetChanged()
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
private inner class PatternsAdapter(
items: List<IgnoreCode>
) : ArrayAdapter<IgnoreCode>(
this,
android.R.layout.simple_list_item_2,
android.R.id.text1,
items
) {
override fun getView(
position: Int,
convertView: View?,
parent: ViewGroup
): View {
val view = super.getView(position, convertView, parent)
val item = patterns[position]
val title = view.findViewById<TextView>(android.R.id.text1)
val subtitle = view.findViewById<TextView>(android.R.id.text2)
title.text = item.pattern
subtitle.text = item.format?.prettifyFormatName() ?: getString(
R.string.all_formats
)
return view
}
}
}
@@ -1,162 +0,0 @@
package de.markusfisch.android.binaryeye.activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.PERMISSION_LOCATION
import de.markusfisch.android.binaryeye.app.PERMISSION_WRITE
import de.markusfisch.android.binaryeye.app.applyLocale
import de.markusfisch.android.binaryeye.app.permissionGrantedCallback
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.app.setFragment
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.fragment.DecodeFragment
import de.markusfisch.android.binaryeye.fragment.EncodeFragment
import de.markusfisch.android.binaryeye.fragment.HistoryFragment
import de.markusfisch.android.binaryeye.fragment.PreferencesFragment
import de.markusfisch.android.binaryeye.net.isEncodeDeeplink
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
import de.markusfisch.android.binaryeye.view.initBars
class MainActivity : AppCompatActivity() {
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
when (requestCode) {
PERMISSION_LOCATION, PERMISSION_WRITE -> if (
grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED
) {
permissionGrantedCallback?.invoke()
permissionGrantedCallback = null
}
}
}
override fun onSupportNavigateUp(): Boolean {
val fm = supportFragmentManager
if (fm != null && fm.backStackEntryCount > 0) {
fm.popBackStack()
} else {
finish()
}
return true
}
override fun attachBaseContext(base: Context?) {
base?.applyLocale(prefs.customLocale)
super.attachBaseContext(base)
}
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setContentView(R.layout.activity_main)
initBars()
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportFragmentManager.addOnBackStackChangedListener {
colorSystemAndToolBars(this@MainActivity)
}
if (state == null) {
supportFragmentManager?.setFragment(getFragmentForIntent(intent))
}
}
companion object {
private const val PREFERENCES = "preferences"
private const val HISTORY = "history"
private const val ENCODE = "encode"
const val DECODED = "decoded"
private fun getFragmentForIntent(intent: Intent?): Fragment {
intent ?: return PreferencesFragment()
return when {
intent.hasExtra(PREFERENCES) -> PreferencesFragment()
intent.hasExtra(HISTORY) -> HistoryFragment()
intent.dataString?.let(::isEncodeDeeplink) == true -> {
val uri = Uri.parse(intent.dataString)
EncodeFragment.newInstance(
content = uri.getQueryParameter("content"),
format = uri.getQueryParameter("format")?.uppercase(),
execute = uri.getQueryParameter("execute")
.let { it == "" || it.toBoolean() }
)
}
intent.hasExtra(ENCODE) -> EncodeFragment.newInstance(
intent.getStringExtra(ENCODE)
)
intent.hasExtra(DECODED) -> intent.getScanExtra(DECODED)?.let {
DecodeFragment.newInstance(it)
} ?: DecodeFragment()
else -> PreferencesFragment()
}
}
fun getPreferencesIntent(context: Context): Intent {
val intent = Intent(context, MainActivity::class.java)
intent.putExtra(PREFERENCES, true)
return intent
}
fun getHistoryIntent(context: Context): Intent {
val intent = Intent(context, MainActivity::class.java)
intent.putExtra(HISTORY, true)
return intent
}
fun getEncodeIntent(
context: Context,
text: String? = "",
isExternal: Boolean = false
): Intent {
val intent = Intent(context, MainActivity::class.java)
intent.putExtra(ENCODE, text)
if (isExternal) {
val flagActivityClearTask = if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB
) {
Intent.FLAG_ACTIVITY_CLEAR_TASK
} else 0
intent.addFlags(
Intent.FLAG_ACTIVITY_NO_HISTORY or
flagActivityClearTask or
Intent.FLAG_ACTIVITY_NEW_TASK
)
}
return intent
}
fun getDecodeIntent(context: Context, scan: Scan): Intent {
val intent = Intent(context, MainActivity::class.java)
intent.putExtra(DECODED, scan)
return intent
}
}
}
private fun Intent.getScanExtra(name: String): Scan? = try {
if (
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
) {
@Suppress("DEPRECATION")
getParcelableExtra(name)
} else {
getParcelableExtra(name, Scan::class.java)
}
} catch (_: Throwable) {
null
}
@@ -1,43 +1,32 @@
package de.markusfisch.android.binaryeye.fragment
package de.markusfisch.android.binaryeye.activity
import android.content.Context
import android.net.wifi.WifiManager
import android.net.wifi.WifiNetworkSuggestion
import android.os.Build
import android.os.Bundle
import android.support.annotation.RequiresApi
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ListView
import androidx.annotation.RequiresApi
import androidx.core.util.size
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
import de.markusfisch.android.binaryeye.widget.toast
class NetworkSuggestionsFragment : Fragment() {
class NetworkSuggestionsActivity : AbstractBaseActivity() {
@RequiresApi(Build.VERSION_CODES.R)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?
): View? {
val ac = activity ?: return null
ac.setTitle(R.string.network_suggestions)
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setScreenContentView(R.layout.activity_network_suggestions)
setTitle(R.string.network_suggestions)
val view = inflater.inflate(
R.layout.fragment_network_suggestions,
container,
false
)
val wm = ac.applicationContext.getSystemService(
val wm = applicationContext.getSystemService(
Context.WIFI_SERVICE
) as WifiManager
val suggestionArrayAdapter = ArrayAdapter(
ac,
this,
android.R.layout.simple_list_item_checked,
wm.networkSuggestions.map {
Suggestion(
@@ -47,18 +36,18 @@ class NetworkSuggestionsFragment : Fragment() {
}
)
val listView = view.findViewById<ListView>(R.id.suggestions)
listView.emptyView = view.findViewById(R.id.no_suggestions)
val listView = findViewById<ListView>(R.id.suggestions)
listView.emptyView = findViewById(R.id.no_suggestions)
listView.adapter = suggestionArrayAdapter
listView.setOnScrollListener(systemBarListViewScrollListener)
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
listView.setPaddingFromWindowInsets()
view.findViewById<View>(R.id.remove).setOnClickListener {
findViewById<View>(R.id.remove).setOnClickListener {
val removeList = ArrayList<WifiNetworkSuggestion>()
val removeFromAdapter = ArrayList<Suggestion>()
val checked = listView.checkedItemPositions
val size = checked.size()
val size = checked.size
for (i in 0 until size) {
if (checked.valueAt(i)) {
val pos = checked.keyAt(i)
@@ -76,13 +65,11 @@ class NetworkSuggestionsFragment : Fragment() {
}
wm.removeNetworkSuggestions(removeList)
} else {
ac.toast(
toast(
R.string.clear_network_suggestions_nothing_to_remove
)
}
}
return view
}
}
@@ -9,9 +9,10 @@ import android.graphics.RectF
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.applyLocale
import de.markusfisch.android.binaryeye.app.prefs
@@ -109,7 +110,7 @@ class PickActivity : AppCompatActivity() {
cropHandleName = "picker_crop_handle"
}
findViewById(R.id.scan).setOnClickListener {
findViewById<View>(R.id.scan).setOnClickListener {
showResult()
}
}
@@ -0,0 +1,34 @@
package de.markusfisch.android.binaryeye.activity
import android.content.Context
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.applyLocale
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.fragment.PreferencesFragment
import de.markusfisch.android.binaryeye.view.initBars
class PreferencesActivity : AppCompatActivity() {
override fun attachBaseContext(base: Context?) {
base?.applyLocale(prefs.customLocale)
super.attachBaseContext(base)
}
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setContentView(R.layout.activity_main)
initBars()
supportActionBar?.setDisplayHomeAsUpEnabled(true)
if (state == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.content_frame, PreferencesFragment())
.commit()
}
}
override fun onSupportNavigateUp(): Boolean {
finish()
return true
}
}
@@ -1,46 +1,36 @@
package de.markusfisch.android.binaryeye.fragment
package de.markusfisch.android.binaryeye.activity
import android.app.AlertDialog
import android.content.Context
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.ListView
import android.widget.TextView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
import de.markusfisch.android.binaryeye.widget.toast
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
class ProfilesFragment : Fragment() {
class ProfilesActivity : AbstractBaseActivity() {
private val profiles = ArrayList<ProfileItem>()
private lateinit var adapter: ProfilesAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?
): View? {
val ac = activity ?: return null
ac.setTitle(R.string.profile)
val view = inflater.inflate(
R.layout.fragment_profiles,
container,
false
)
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setScreenContentView(R.layout.activity_profiles)
setTitle(R.string.profile)
refreshProfiles()
val listView = view.findViewById<ListView>(R.id.profiles)
listView.emptyView = view.findViewById(R.id.no_profiles)
adapter = ProfilesAdapter(ac, profiles)
val listView = findViewById<ListView>(R.id.profiles)
listView.emptyView = findViewById(R.id.no_profiles)
adapter = ProfilesAdapter(this, profiles)
listView.adapter = adapter
listView.setOnScrollListener(systemBarListViewScrollListener)
listView.setOnItemClickListener { _, _, position, _ ->
@@ -51,14 +41,12 @@ class ProfilesFragment : Fragment() {
true
}
view.findViewById<View>(R.id.add).setOnClickListener {
findViewById<View>(R.id.add).setOnClickListener {
addProfile()
}
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
listView.setPaddingFromWindowInsets()
return view
}
private fun refreshProfiles() {
@@ -66,11 +54,11 @@ class ProfilesFragment : Fragment() {
profiles.add(
ProfileItem(
null,
getString(R.string.profile_default)
prefs.profileLabel(this, null)
)
)
prefs.profiles.forEach { name ->
profiles.add(ProfileItem(name, name))
profiles.add(ProfileItem(name, prefs.profileLabel(this, name)))
}
if (this::adapter.isInitialized) {
adapter.notifyDataSetChanged()
@@ -78,48 +66,45 @@ class ProfilesFragment : Fragment() {
}
private fun selectProfile(position: Int) {
val ac = activity ?: return
val profile = profiles[position].name
if (prefs.profile == profile) {
return
}
prefs.load(ac, profile)
prefs.load(this, profile)
adapter.notifyDataSetChanged()
}
// Dialogs do not have a parent view.
@Suppress("InflateParams")
private fun addProfile() {
val ac = activity ?: return
val view = ac.layoutInflater.inflate(
val view = layoutInflater.inflate(
R.layout.dialog_profile_name, null
)
val editText = view.findViewById<EditText>(R.id.name)
AlertDialog.Builder(ac)
MaterialAlertDialogBuilder(this)
.setView(view)
.setPositiveButton(android.R.string.ok) { _, _ ->
val name = editText.text.toString().trim()
if (name.isNotEmpty() && prefs.addProfile(name)) {
prefs.load(ac, name)
prefs.load(this, name)
refreshProfiles()
} else {
ac.toast(R.string.profile_invalid_name)
toast(R.string.profile_invalid_name)
}
}
.show()
}
private fun confirmRemoveProfile(position: Int) {
val ac = activity ?: return
val profile = profiles[position]
val profileName = profile.name ?: return
AlertDialog.Builder(ac)
MaterialAlertDialogBuilder(this)
.setMessage(R.string.profile_remove_confirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
val isCurrent = prefs.profile == profileName
prefs.removeProfile(profileName)
if (isCurrent) {
prefs.load(ac, null)
prefs.load(this, null)
}
refreshProfiles()
}
@@ -132,8 +117,8 @@ class ProfilesFragment : Fragment() {
private val items: List<ProfileItem>
) : ArrayAdapter<ProfileItem>(
context,
android.R.layout.simple_list_item_2,
android.R.id.text1,
R.layout.row_profile,
R.id.title,
items
) {
override fun getView(
@@ -145,6 +130,7 @@ class ProfilesFragment : Fragment() {
val item = items[position]
val isCurrent = item.name == prefs.profile
getViewHolder(view).apply {
current = item
title.text = item.label
if (isCurrent) {
subtitle.text = context.getString(R.string.profile_current)
@@ -160,16 +146,31 @@ class ProfilesFragment : Fragment() {
private fun getViewHolder(
view: View
): ViewHolder = view.tag as ViewHolder? ?: ViewHolder(
view.findViewById(android.R.id.text1),
view.findViewById(android.R.id.text2),
).also {
view.tag = it
view.findViewById(R.id.title),
view.findViewById(R.id.subtitle),
view.findViewById(R.id.share_qr),
).also { holder ->
holder.shareQr.setOnClickListener {
val item = holder.current ?: return@setOnClickListener
context.startActivity(
BarcodeActivity.newIntent(
context,
prefs.toJson(context, item.name),
BarcodeFormat.QRCode,
addQuietZone = true
)
)
}
view.tag = holder
}
private data class ViewHolder(
private class ViewHolder(
val title: TextView,
val subtitle: TextView,
)
val shareQr: View,
) {
var current: ProfileItem? = null
}
}
}
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.activity
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import androidx.appcompat.app.AppCompatActivity
class SplashActivity : AppCompatActivity() {
override fun onCreate(state: Bundle?) {
@@ -1,7 +1,8 @@
package de.markusfisch.android.binaryeye.activity
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.prefs
@@ -9,23 +10,12 @@ class WelcomeActivity : AppCompatActivity() {
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setContentView(R.layout.activity_welcome)
findViewById(R.id.expert).setOnClickListener {
findViewById<View>(R.id.expert).setOnClickListener {
prefs.loadExpertProfile(this)
finish()
}
findViewById(R.id.simple).setOnClickListener {
prefs.apply {
showCropHandle = false
zoomBySwiping = false
beep = true
copyImmediately = true
openImmediately = true
showMetaData = false
showHexDump = false
defaultSearchUrl = "https://www.google.com/search?q="
openWithUrl = defaultSearchUrl
expandEscapeSequences = false
brightenScreen = true
}
findViewById<View>(R.id.simple).setOnClickListener {
prefs.loadSimpleProfile(this)
finish()
}
}
@@ -2,11 +2,14 @@ package de.markusfisch.android.binaryeye.adapter
import android.content.Context
import android.database.Cursor
import android.support.v4.content.ContextCompat
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.CursorAdapter
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.Spinner
import android.widget.TextView
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.database.Database
@@ -15,18 +18,19 @@ import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
class ScansAdapter(context: Context, cursor: Cursor) :
class ScansAdapter(
context: Context,
cursor: Cursor,
private val onPinClick: (Long, Boolean) -> Unit
) :
CursorAdapter(context, cursor, false) {
private val idIndex = cursor.getColumnIndex(Database.SCANS_ID)
private val pinnedIndex = cursor.getColumnIndex(Database.SCANS_PINNED)
private val timeIndex = cursor.getColumnIndex(Database.SCANS_DATETIME)
private val nameIndex = cursor.getColumnIndex(Database.SCANS_NAME)
private val textIndex = cursor.getColumnIndex(Database.SCANS_TEXT)
private val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
private val selections = mutableMapOf<Long, Int>()
private val selectedColor = ContextCompat.getColor(
context, R.color.selected_row
)
fun select(view: View, id: Long, position: Int) {
view.select(
if (selections[id] == null) {
@@ -89,8 +93,16 @@ class ScansAdapter(context: Context, cursor: Cursor) :
context: Context,
cursor: Cursor
) {
val id = cursor.getLong(idIndex)
val pinned = cursor.getInt(pinnedIndex) != 0
val format = cursor.getString(formatIndex)
getViewHolder(view).apply {
timeView.text = formatDateTime(cursor.getString(timeIndex))
metaView.text = context.getString(
R.string.scan_meta_data,
formatDateTime(cursor.getString(timeIndex)),
format.prettifyFormatName()
)
formatIconView.setImageResource(format.toBarcodeIcon())
val name = cursor.getString(nameIndex)
val text = cursor.getString(textIndex)
var icon = 0
@@ -109,9 +121,27 @@ class ScansAdapter(context: Context, cursor: Cursor) :
contentView.setCompoundDrawablesWithIntrinsicBounds(
icon, 0, 0, 0
)
formatView.text = cursor.getString(formatIndex).prettifyFormatName()
pinView.apply {
setImageResource(
if (pinned) {
R.drawable.ic_action_pin
} else {
R.drawable.ic_action_pin_outline
}
)
contentDescription = context.getString(
if (pinned) {
R.string.unpin_item
} else {
R.string.pin_item
}
)
setOnClickListener {
onPinClick(id, !pinned)
}
}
}
val selected = selections[cursor.getLong(idIndex)] != null
val selected = selections[id] != null
view.post {
// Needs to be put on the queue to work.
view.select(selected)
@@ -119,27 +149,107 @@ class ScansAdapter(context: Context, cursor: Cursor) :
}
private fun View.select(selected: Boolean) {
setBackgroundColor(if (selected) selectedColor else 0)
isActivated = selected
refreshDrawableState()
}
private fun getViewHolder(
view: View
): ViewHolder = view.tag as ViewHolder? ?: ViewHolder(
view.findViewById(R.id.time),
view.findViewById(R.id.format_icon),
view.findViewById(R.id.meta),
view.findViewById(R.id.content),
view.findViewById(R.id.format)
view.findViewById(R.id.pin)
).also {
view.tag = it
}
private data class ViewHolder(
val timeView: TextView,
val formatIconView: ImageView,
val metaView: TextView,
val contentView: TextView,
val formatView: TextView
val pinView: ImageButton
)
}
private val formatWordBoundary =
"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?<=[A-Za-z])(?=\\d)|(?<=\\d)(?=[A-Za-z])".toRegex()
fun Context.setupFormatSpinner(spinner: Spinner): List<String> {
val names = resources.getStringArray(
R.array.barcode_formats_names
).toMutableList().apply {
add(0, getString(R.string.all_formats))
}
val values = resources.getStringArray(
R.array.barcode_formats_values
).toMutableList().apply {
add(0, "")
}
spinner.adapter = ArrayAdapter(
this,
android.R.layout.simple_spinner_item,
names
).apply {
setDropDownViewResource(
android.R.layout.simple_spinner_dropdown_item
)
}
return values
}
fun String.prettifyFormatName() = replace("_", " ")
.replace(formatWordBoundary, " ")
fun String.toFormatDescriptionResId(): Int = when (this) {
"Aztec" -> R.string.format_description_aztec
"Codabar" -> R.string.format_description_codabar
"Code39",
"Code39Std" -> R.string.format_description_code39
"Code39Ext" -> R.string.format_description_code39ext
"Code32" -> R.string.format_description_code32
"PZN" -> R.string.format_description_pzn
"Code93" -> R.string.format_description_code93
"Code128" -> R.string.format_description_code128
"DataBar" -> R.string.format_description_databar
"DataBarOmni" -> R.string.format_description_databar_omni
"DataBarStk" -> R.string.format_description_databar_stk
"DataBarLtd" -> R.string.format_description_databar_ltd
"DataBarExp" -> R.string.format_description_databar_exp
"DataBarExpStk" -> R.string.format_description_databar_exp_stk
"DataMatrix" -> R.string.format_description_data_matrix
"DXFilmEdge" -> R.string.format_description_dx_film_edge
"EAN8" -> R.string.format_description_ean8
"EAN13" -> R.string.format_description_ean13
"ITF" -> R.string.format_description_itf
"MaxiCode" -> R.string.format_description_maxicode
"PDF417" -> R.string.format_description_pdf417
"Telepen" -> R.string.format_description_telepen
"TelepenAlpha" -> R.string.format_description_telepen_alpha
"TelepenNumeric" -> R.string.format_description_telepen_numeric
"QRCode" -> R.string.format_description_qr_code
"MicroQRCode" -> R.string.format_description_micro_qr_code
"RMQRCode" -> R.string.format_description_rmqr_code
"UPCA" -> R.string.format_description_upca
"UPCE" -> R.string.format_description_upce
else -> 0
}
private fun String?.toBarcodeIcon(): Int = when (this) {
"QRCode",
"RMQRCode" -> R.drawable.ic_barcode_qr_code
"MicroQRCode" -> R.drawable.ic_barcode_micro_qr
"Aztec" -> R.drawable.ic_barcode_aztec
"DataMatrix",
"MaxiCode" -> R.drawable.ic_barcode_data_matrix
"PDF417" -> R.drawable.ic_barcode_pdf417
else -> R.drawable.ic_barcode_linear
}
private fun formatDateTime(rfc: String): String {
try {
@@ -1,7 +1,7 @@
package de.markusfisch.android.binaryeye.app
import android.app.AlertDialog
import android.content.Context
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
@@ -9,10 +9,10 @@ import kotlin.coroutines.suspendCoroutine
suspend inline fun <T : Any> alertDialog(
context: Context,
crossinline build: AlertDialog.Builder.(resume: (T?) -> Unit) -> Unit
crossinline build: MaterialAlertDialogBuilder.(resume: (T?) -> Unit) -> Unit
): T? = withContext(Dispatchers.Main) {
suspendCoroutine { continuation ->
AlertDialog.Builder(context).apply {
MaterialAlertDialogBuilder(context).apply {
setOnCancelListener {
continuation.resume(null)
}
@@ -1,6 +1,7 @@
package de.markusfisch.android.binaryeye.app
import android.app.Application
import com.google.android.material.color.DynamicColors
import de.markusfisch.android.binaryeye.database.Database
import de.markusfisch.android.binaryeye.preference.Preferences
@@ -10,7 +11,11 @@ val prefs = Preferences()
class BinaryEyeApp : Application() {
override fun onCreate() {
super.onCreate()
db.open(this)
prefs.init(this)
DynamicColors.applyToActivitiesIfAvailable(
this,
DynamicColors.Precondition { _, _ -> prefs.dynamicColors }
)
db.open(this)
}
}
@@ -1,19 +0,0 @@
package de.markusfisch.android.binaryeye.app
import android.annotation.SuppressLint
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import de.markusfisch.android.binaryeye.R
fun FragmentManager.setFragment(fragment: Fragment) {
getTransaction(fragment).commit()
}
fun FragmentManager.addFragment(fragment: Fragment) {
getTransaction(fragment).addToBackStack(null).commit()
}
@SuppressLint("CommitTransaction")
private fun FragmentManager.getTransaction(
fragment: Fragment
) = beginTransaction().replace(R.id.content_frame, fragment)
@@ -4,8 +4,8 @@ import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.os.Build
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
var permissionGrantedCallback: (() -> Any)? = null
@@ -9,6 +9,7 @@ import java.util.Locale
data class AutomatedAction(
val pattern: String,
val type: Type,
val format: String? = null,
val urlTemplate: String? = null,
val intentUriTemplate: String? = null
) {
@@ -16,8 +17,9 @@ data class AutomatedAction(
Intent, CustomIntent
}
fun matches(content: String): Boolean = try {
Regex(pattern).matches(content)
fun matches(content: String, format: String): Boolean = try {
(this.format == null || this.format == format) &&
Regex(pattern).matches(content)
} catch (_: Exception) {
false
}
@@ -25,6 +27,7 @@ data class AutomatedAction(
fun toJson(): JSONObject = JSONObject().apply {
put(KEY_PATTERN, pattern)
put(KEY_TYPE, type.name.lowercase(Locale.ROOT))
put(KEY_FORMAT, format ?: "")
put(KEY_URL_TEMPLATE, urlTemplate ?: "")
put(KEY_INTENT_URI_TEMPLATE, intentUriTemplate ?: "")
}
@@ -32,6 +35,7 @@ data class AutomatedAction(
companion object {
private const val KEY_PATTERN = "pattern"
private const val KEY_TYPE = "type"
private const val KEY_FORMAT = "format"
private const val KEY_URL_TEMPLATE = "url_template"
private const val KEY_INTENT_URI_TEMPLATE = "intent_uri_template"
@@ -46,6 +50,7 @@ data class AutomatedAction(
"html" -> return null
else -> Type.Intent
}
val format = obj.optString(KEY_FORMAT, "").trim()
val urlTemplate = obj.optString(KEY_URL_TEMPLATE, "").trim()
val intentUriTemplate = obj.optString(
KEY_INTENT_URI_TEMPLATE,
@@ -54,6 +59,7 @@ data class AutomatedAction(
return AutomatedAction(
pattern = pattern,
type = type,
format = format.ifEmpty { null },
urlTemplate = urlTemplate.ifEmpty { null },
intentUriTemplate = intentUriTemplate.ifEmpty { null }
)
@@ -4,8 +4,8 @@ import android.content.Context
import android.content.Intent
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.content.execShareIntent
import de.markusfisch.android.binaryeye.content.openUrl
import de.markusfisch.android.binaryeye.content.startIntentOrToast
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.widget.toast
@@ -19,7 +19,7 @@ fun Context.runAutomatedActions(scan: Scan): Boolean {
return false
}
for (action in actions) {
if (action.matches(content)) {
if (action.matches(content, scan.format.name)) {
return when (action.type) {
AutomatedAction.Type.Intent -> {
val url = action.urlTemplate?.buildUrl(scan)
@@ -55,5 +55,5 @@ private fun Context.execCustomIntent(
toast(R.string.automated_action_intent_uri_invalid)
return false
}
return execShareIntent(intent)
return startIntentOrToast(intent)
}
@@ -3,7 +3,7 @@ package de.markusfisch.android.binaryeye.bluetooth
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothSocket
import android.support.v7.preference.ListPreference
import androidx.preference.ListPreference
import de.markusfisch.android.binaryeye.database.Scan
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -1,6 +1,7 @@
package de.markusfisch.android.binaryeye.content
import android.graphics.Bitmap
import android.util.Size
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.graphics.COLOR_BLACK
import de.markusfisch.android.binaryeye.graphics.COLOR_WHITE
@@ -48,7 +49,8 @@ fun Scan.toBarcode(): Barcode<*> {
ContentBarcode(
content,
format,
errorCorrectionLevel.toErrorCorrectionInt()
errorCorrectionLevel.toErrorCorrectionInt(),
addQuietZone = true
)
}
}
@@ -108,21 +110,24 @@ class ContentBarcode<T>(
content: T,
format: BarcodeFormat,
val ecLevel: Int = -1,
val margin: Int = 1,
val addQuietZone: Boolean = false,
colors: BarcodeColors = BarcodeColors.BLACK_ON_WHITE
) : Barcode<T>(content, format, colors) {
override fun toBitmap(size: Int) = ZxingCpp.encodeAsBitmap(
content, format, size, format.height(size), margin, ecLevel,
setColor = colors.foregroundColor(),
unsetColor = colors.backgroundColor()
)
override fun toBitmap(size: Int): Bitmap {
val s = format.resolveSize(size)
return ZxingCpp.encodeAsBitmap(
content, format, s.width, s.height, addQuietZone, ecLevel,
setColor = colors.foregroundColor(),
unsetColor = colors.backgroundColor()
)
}
override fun toSvg() = ZxingCpp.encodeAsSvg(
content, format, margin, ecLevel
content, format, addQuietZone, ecLevel
)
override fun toText() = ZxingCpp.encodeAsText(
content, format, margin, ecLevel,
content, format, addQuietZone, ecLevel,
inverted = colors == BarcodeColors.BLACK_ON_WHITE
)
}
@@ -184,20 +189,33 @@ private fun BitMatrix.inflate(
return scaled
}
private fun BarcodeFormat.height(size: Int) = when (this) {
private fun BarcodeFormat.resolveSize(size: Int): Size = when (this) {
// 1D barcodes don't need as much vertical space.
BarcodeFormat.CODABAR,
BarcodeFormat.CODE_39,
BarcodeFormat.CODE_93,
BarcodeFormat.CODE_128,
BarcodeFormat.DATA_BAR,
BarcodeFormat.DATA_BAR_EXPANDED,
BarcodeFormat.DX_FILM_EDGE,
BarcodeFormat.EAN_8,
BarcodeFormat.EAN_13,
BarcodeFormat.Codabar,
BarcodeFormat.Code39,
BarcodeFormat.Code39Std,
BarcodeFormat.Code39Ext,
BarcodeFormat.Code32,
BarcodeFormat.PZN,
BarcodeFormat.Code93,
BarcodeFormat.Code128,
BarcodeFormat.DataBarOmni,
BarcodeFormat.DataBarStk,
BarcodeFormat.DataBarLtd,
BarcodeFormat.DataBarExp,
BarcodeFormat.DataBarExpStk,
BarcodeFormat.DXFilmEdge,
BarcodeFormat.EAN8,
BarcodeFormat.EAN13,
BarcodeFormat.ITF,
BarcodeFormat.UPC_A,
BarcodeFormat.UPC_E -> size / 3
BarcodeFormat.Telepen,
BarcodeFormat.TelepenAlpha,
BarcodeFormat.TelepenNumeric,
BarcodeFormat.UPCA,
BarcodeFormat.UPCE -> Size(size, size / 3)
else -> size
}
// PDF417 requires more horizontal space.
BarcodeFormat.PDF417 -> Size(size * 3, size / 2)
else -> Size(size, size)
}
@@ -8,16 +8,11 @@ import android.os.PersistableBundle
fun Context.copyToClipboard(text: String, isSensitive: Boolean = false) {
val service = getSystemService(Context.CLIPBOARD_SERVICE)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
@Suppress("DEPRECATION")
(service as android.text.ClipboardManager).text = text
} else {
(service as android.content.ClipboardManager).setPrimaryClip(
ClipData.newPlainText("plain text", text).apply {
setSensitive(isSensitive)
}
)
}
(service as android.content.ClipboardManager).setPrimaryClip(
ClipData.newPlainText("plain text", text).apply {
setSensitive(isSensitive)
}
)
}
private fun ClipData.setSensitive(isSensitive: Boolean) {
@@ -0,0 +1,23 @@
package de.markusfisch.android.binaryeye.content
import android.content.Context
import de.markusfisch.android.binaryeye.R
private val elementToLabel = mapOf(
EpcQrElement.SERVICE_TAG to R.string.epc_service_tag,
EpcQrElement.VERSION to R.string.epc_version,
EpcQrElement.CHARSET to R.string.epc_charset,
EpcQrElement.IDENTIFICATION to R.string.epc_identification,
EpcQrElement.BIC to R.string.epc_bic,
EpcQrElement.NAME to R.string.epc_name,
EpcQrElement.IBAN to R.string.epc_iban,
EpcQrElement.AMOUNT to R.string.epc_amount,
EpcQrElement.PURPOSE to R.string.epc_purpose,
EpcQrElement.REFERENCE to R.string.epc_reference,
EpcQrElement.TEXT to R.string.epc_text,
EpcQrElement.INFORMATION to R.string.epc_information
)
fun Context.epcQrToRes(element: EpcQrElement): String {
return elementToLabel[element]?.let { getString(it) } ?: element.name
}
@@ -0,0 +1,141 @@
package de.markusfisch.android.binaryeye.content
enum class EpcQrElement {
SERVICE_TAG, VERSION, CHARSET, IDENTIFICATION,
BIC, NAME, IBAN, AMOUNT, PURPOSE, REFERENCE, TEXT, INFORMATION
}
object EpcQrParser {
private val BIC_REGEX = Regex("[A-Z0-9]{8}([A-Z0-9]{3})?")
private val IBAN_REGEX = Regex("[A-Z]{2}[0-9A-Z]{13,32}")
private val AMOUNT_REGEX = Regex("EUR\\d{1,10}(\\.\\d{2})?")
private val PURPOSE_REGEX = Regex("[A-Z]{4}")
private val charsetMap = mapOf(
"1" to "UTF-8",
"2" to "ISO 8859-1",
"3" to "ISO 8859-2",
"4" to "ISO 8859-4",
"5" to "ISO 8859-5",
"6" to "ISO 8859-7",
"7" to "ISO 8859-10",
"8" to "ISO 8859-15"
)
private fun splitLines(s: String): List<String>? {
val normalized = s
.replace("\r\n", "\n")
.replace('\r', '\n')
.removeSuffix("\n")
if (normalized.isEmpty()) {
return null
}
return normalized.split('\n')
}
private fun isValidVersion(value: String): Boolean {
return value == "001" || value == "002"
}
private fun isValidCharset(value: String): Boolean {
return charsetMap.containsKey(value)
}
private fun isValidIdentification(value: String): Boolean {
return value == "SCT"
}
private fun isValidBic(value: String): Boolean {
return value.matches(BIC_REGEX)
}
private fun isValidIban(value: String): Boolean {
return value.matches(IBAN_REGEX)
}
private fun isValidAmount(value: String): Boolean {
return value.matches(AMOUNT_REGEX)
}
fun parse(s: String?): Map<EpcQrElement, String>? {
if (s.isNullOrEmpty()) {
return null
}
val lines = splitLines(s) ?: return null
if (lines.size !in 7..12) {
return null
}
if (lines[0] != "BCD") {
return null
}
if (!isValidVersion(lines[1])) {
return null
}
if (!isValidCharset(lines[2])) {
return null
}
if (!isValidIdentification(lines[3])) {
return null
}
val bic = lines.getOrElse(4) { "" }
if ((lines[1] == "001" || bic.isNotEmpty()) && !isValidBic(bic)) {
return null
}
val recipient = lines.getOrElse(5) { "" }
if (recipient.isBlank() || recipient.length > 70) {
return null
}
val iban = lines.getOrElse(6) { "" }
if (!isValidIban(iban)) {
return null
}
val amount = lines.getOrElse(7) { "" }
if (amount.isNotEmpty() && !isValidAmount(amount)) {
return null
}
val purpose = lines.getOrElse(8) { "" }
if (purpose.isNotEmpty() && !purpose.matches(PURPOSE_REGEX)) {
return null
}
val remittanceReference = lines.getOrElse(9) { "" }
if (remittanceReference.length > 25) {
return null
}
val remittanceText = lines.getOrElse(10) { "" }
if (remittanceText.length > 140) {
return null
}
if (remittanceReference.isNotEmpty() && remittanceText.isNotEmpty()) {
return null
}
val information = lines.getOrElse(11) { "" }
if (information.length > 70) {
return null
}
return linkedMapOf(
EpcQrElement.SERVICE_TAG to lines[0],
EpcQrElement.VERSION to lines[1],
EpcQrElement.CHARSET to (charsetMap[lines[2]] ?: lines[2]),
EpcQrElement.IDENTIFICATION to lines[3],
EpcQrElement.BIC to bic,
EpcQrElement.NAME to recipient,
EpcQrElement.IBAN to iban,
EpcQrElement.AMOUNT to amount.removePrefix("EUR"),
EpcQrElement.PURPOSE to purpose,
EpcQrElement.REFERENCE to remittanceReference,
EpcQrElement.TEXT to remittanceText,
EpcQrElement.INFORMATION to information
)
}
}
@@ -0,0 +1,191 @@
package de.markusfisch.android.binaryeye.content
import android.content.Context
import de.markusfisch.android.binaryeye.R
import de.tsenger.vdstools.DataEncoder
import de.tsenger.vdstools.Verifier
import de.tsenger.vdstools.idb.IdbSeal
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.icao.CscaMasterList
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import org.bouncycastle.asn1.x9.ECNamedCurveTable
import org.bouncycastle.asn1.x9.X962Parameters
import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.cms.CMSSignedData
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.Provider
import java.security.Security
import java.security.cert.X509Certificate
import java.util.Date
object IdbVerifier {
private const val MASTER_LIST_ASSET = "DE_ML_2026-01-08-12-20-54.ml"
private val provider: Provider by lazy {
BouncyCastleProvider().also { bcProvider ->
if (Security.getProvider(bcProvider.name) == null) {
Security.addProvider(bcProvider)
}
}
}
@Volatile
private var cachedMasterListCertificates: List<X509Certificate>? = null
fun verify(context: Context, seal: IdbSeal): List<SealField> {
val signatureInfo = seal.signatureInfo
?: return listOf(
SealField(
R.string.idb_verification,
context.getString(R.string.idb_verification_unsigned)
)
)
val headerCertificateReference = seal.signerCertReference
?.hexToByteArrayOrNull()
val signerCertificate = context.findCertificateByReference(
headerCertificateReference
)
val curveName = signerCertificate?.let {
curveNameFrom(it)
}
val signedBytes = seal.signedBytes
val signatureResult = if (
signerCertificate == null ||
curveName.isNullOrBlank() ||
signedBytes == null
) {
Verifier.Result.VerifyError
} else {
Verifier(
signedBytes,
signatureInfo.plainSignatureBytes,
signerCertificate.publicKey.encoded,
curveName
).verify()
}
val referenceMatches = signerCertificate != null &&
headerCertificateReference != null &&
DataEncoder.buildCertificateReference(signerCertificate.encoded)
.contentEquals(headerCertificateReference)
val certificateValidNow = signerCertificate != null && runCatching {
signerCertificate.checkValidity(Date())
true
}.getOrElse {
false
}
val verification = context.getString(
when {
signerCertificate == null ->
R.string.idb_verification_signer_certificate_issuer_not_found_in_master_list
!referenceMatches ->
R.string.idb_verification_signature_invalid
!certificateValidNow ->
R.string.idb_verification_signature_invalid
signatureResult == Verifier.Result.SignatureValid ->
R.string.idb_verification_verified
signatureResult == Verifier.Result.SignatureInvalid ->
R.string.idb_verification_signature_invalid
else -> R.string.idb_verification_error
}
)
val fields = mutableListOf(
SealField(R.string.idb_verification, verification),
SealField(
R.string.idb_certificate_reference,
signatureInfo.signerCertificateReference
),
SealField(
R.string.vds_signing_date,
signatureInfo.signingDate.toString()
),
SealField(
R.string.idb_signature_verification,
signatureResult.name
),
)
if (signerCertificate != null) {
fields.add(
SealField(
R.string.idb_signer_certificate_subject,
signerCertificate.subjectX500Principal.name
)
)
fields.add(
SealField(
R.string.idb_signer_certificate_issuer,
signerCertificate.issuerX500Principal.name
)
)
}
return fields
}
private fun String.hexToByteArrayOrNull(): ByteArray? {
if (length % 2 != 0) {
return null
}
return runCatching {
ByteArray(length / 2) { index ->
substring(index * 2, index * 2 + 2).toInt(16).toByte()
}
}.getOrNull()
}
private fun Context.loadMasterListCertificates(): List<X509Certificate> {
cachedMasterListCertificates?.let {
return it
}
synchronized(this) {
cachedMasterListCertificates?.let {
return it
}
val cmsSignedData = assets.open(MASTER_LIST_ASSET).use { input ->
CMSSignedData(input.readBytes())
}
val signedContent = cmsSignedData.signedContent?.content as? ByteArray
?: throw IllegalArgumentException("Master list does not contain signed content")
val masterList = CscaMasterList.getInstance(signedContent)
val certificates = masterList.certStructs.map { cert ->
JcaX509CertificateConverter()
.setProvider(provider)
.getCertificate(X509CertificateHolder(cert))
}
cachedMasterListCertificates = certificates
return certificates
}
}
private fun Context.findCertificateByReference(
certificateReference: ByteArray?
): X509Certificate? {
if (certificateReference == null || certificateReference.size != 5) {
return null
}
return runCatching {
loadMasterListCertificates().firstOrNull { certificate ->
DataEncoder.buildCertificateReference(certificate.encoded)
.contentEquals(certificateReference)
}
}.getOrNull()
}
private fun curveNameFrom(certificate: X509Certificate): String? {
return runCatching {
val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(
certificate.publicKey.encoded
)
val params = X962Parameters.getInstance(subjectPublicKeyInfo.algorithm.parameters)
val oid = params.parameters as? ASN1ObjectIdentifier
oid?.let {
ECNamedCurveTable.getName(it)
}
}.getOrNull()
}
}
@@ -96,15 +96,30 @@ object IdlParser {
val data = findSubType(s, idlInfo)
val len = data.length
var start = 0
var skipSubtype = false
for (p in 0..len) {
if (p < len && data[p] > '\u001f') {
continue
}
if (p - start > 3) {
val vp = start + 3
val key = data.substring(start, vp)
// Per spec, CR (0x0d) is the subfile boundary marker. The segment
// immediately following a CR may begin with a 2-char subfile type
// identifier (e.g. "ZV", "ZI") prepended to the first field.
// Detect this by checking that the marker matches the key prefix
// (jurisdiction fields always share their 2-char subfile prefix).
val seg = if (skipSubtype && p - start >= 4 &&
data[start] == data[start + 2] &&
data[start + 1] == data[start + 3]
) {
start + 2
} else {
start
}
if (p - seg > 3) {
val vp = seg + 3
val key = data.substring(seg, vp)
if (key.matches(Regex("[DZ][A-Z]{2}"))) {
var value = data.substring(vp, p)
@@ -114,6 +129,7 @@ object IdlParser {
idlInfo.elements[key] = value.trim()
}
}
skipSubtype = p < len && data[p] == '\r'
start = p + 1
}
@@ -0,0 +1,142 @@
package de.markusfisch.android.binaryeye.content
import android.content.Context
import de.markusfisch.android.binaryeye.R
import de.tsenger.vdstools.asn1.DerTlv
import de.tsenger.vdstools.generic.Message
import de.tsenger.vdstools.generic.MessageCoding
import de.tsenger.vdstools.generic.MessageValue
import de.tsenger.vdstools.generic.Seal
import de.tsenger.vdstools.idb.IdbSeal
import org.json.JSONArray
import org.json.JSONObject
import de.tsenger.vdstools.generic.SealParser as VdsSealParser
data class SealField(val name: Any, val value: String)
private data class IdbChildMessageDefinition(
val name: String,
val coding: MessageCoding
)
object SealParser {
private val idbChildMessagesByName: Map<String, Map<Int, IdbChildMessageDefinition>> by lazy {
loadIdbChildMessagesByName()
}
fun parse(
context: Context,
bytes: ByteArray
): List<SealField>? = try {
context.buildFields(
VdsSealParser().parse(bytes.toString(Charsets.ISO_8859_1))
)
} catch (_: Exception) {
null
}
private fun Context.buildFields(seal: Seal): List<SealField> = buildList {
add(SealField(R.string.vds_document_type, seal.documentType))
add(SealField(R.string.vds_issuing_country, seal.issuingCountry))
if (seal is IdbSeal) {
addAll(IdbVerifier.verify(this@buildFields, seal))
}
for (message in seal.messageList) {
addAll(message.toFields())
}
}
private fun Message.toFields(): List<SealField> = buildList {
add(
if (value is MessageValue.BytesValue) {
SealField(
"$name (${value.rawBytes.size} bytes)",
value.rawBytes.toHexString().uppercase()
)
} else {
SealField(name, value.toString())
}
)
for (child in childMessages()) {
addAll(child.toFields())
}
}
private fun Message.childMessages(): List<Message> {
val childDefinitionsByTag = idbChildMessagesByName[name] ?: return emptyList()
return runCatching {
DerTlv.parseAll(value.rawBytes).mapNotNull { derTlv ->
val childTag = derTlv.tag.toInt() and 0xff
val childDefinition = childDefinitionsByTag[childTag]
?: return@mapNotNull null
Message(
childTag,
childDefinition.name,
childDefinition.coding,
MessageValue.fromBytes(
derTlv.value,
childDefinition.coding
)
)
}
}.getOrElse {
emptyList()
}
}
private fun loadIdbChildMessagesByName(): Map<String, Map<Int, IdbChildMessageDefinition>> {
val resourceConstantsClass = runCatching {
Class.forName("de.tsenger.vdstools.generated.ResourceConstants")
}.getOrElse {
return emptyMap()
}
val json = runCatching {
resourceConstantsClass.getField(
"IDB_MESSAGE_TYPES_JSON"
).get(null) as String
}.getOrElse {
return emptyMap()
}
val childMessagesByName =
mutableMapOf<String, Map<Int, IdbChildMessageDefinition>>()
val messageTypes = JSONArray(json)
for (i in 0 until messageTypes.length()) {
val messageType = messageTypes.getJSONObject(i)
val messages = messageType.optJSONArray("messages") ?: continue
collectChildMessages(
messageType.getString("name"),
messages,
childMessagesByName
)
}
return childMessagesByName
}
private fun collectChildMessages(
parentName: String,
messages: JSONArray,
childMessagesByName: MutableMap<String, Map<Int, IdbChildMessageDefinition>>
) {
val childMessages = mutableMapOf<Int, IdbChildMessageDefinition>()
for (i in 0 until messages.length()) {
val message = messages.getJSONObject(i)
val childDefinition = message.toChildMessageDefinition()
childMessages[message.getInt("tag")] = childDefinition
val nestedMessages = message.optJSONArray("messages") ?: continue
collectChildMessages(
childDefinition.name,
nestedMessages,
childMessagesByName
)
}
if (childMessages.isNotEmpty()) {
childMessagesByName[parentName] = childMessages
}
}
private fun JSONObject.toChildMessageDefinition(): IdbChildMessageDefinition {
return IdbChildMessageDefinition(
getString("name"),
MessageCoding.valueOf(getString("coding"))
)
}
}
@@ -5,15 +5,17 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.support.v4.content.FileProvider
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import de.markusfisch.android.binaryeye.BuildConfig
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.widget.toast
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
fun Context.execShareIntent(intent: Intent): Boolean = if (
fun Context.startIntentOrToast(intent: Intent): Boolean = if (
!startIntent(intent)
) {
toast(R.string.cannot_resolve_action)
@@ -42,28 +44,27 @@ fun Context.openUrl(url: String, silent: Boolean = false): Boolean {
}
fun Context.openUri(uri: Uri, silent: Boolean = false): Boolean {
val intent = Intent(Intent.ACTION_VIEW, uri).apply {
if (uri.scheme == "content") {
val cleaned = if (prefs.stripTrackingParams) {
uri.stripTrackingParams()
} else {
uri
}
val intent = Intent(Intent.ACTION_VIEW, cleaned).apply {
if (cleaned.scheme == "content") {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
}
return if (silent) {
startIntent(intent)
} else {
execShareIntent(intent)
startIntentOrToast(intent)
}
}
fun String.parseAndNormalizeUri(): Uri = Uri.parse(this).let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
it.normalizeScheme()
} else {
it
}
}
fun String.parseAndNormalizeUri(): Uri = toUri().normalizeScheme()
fun Context.shareText(text: String, mimeType: String = "text/plain") {
execShareIntent(Intent(Intent.ACTION_SEND).apply {
startIntentOrToast(Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_TEXT, text)
type = mimeType
})
@@ -126,7 +127,7 @@ private fun Context.getUriForFile(file: File): Uri? = if (
}
private fun Context.shareUri(uri: Uri, mimeType: String) {
execShareIntent(Intent(Intent.ACTION_SEND).apply {
startIntentOrToast(Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
type = mimeType
@@ -0,0 +1,44 @@
package de.markusfisch.android.binaryeye.content
import android.net.Uri
private val TRACKING_PARAMS = setOf(
"utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content",
"utm_id", "utm_name", "utm_referrer", "utm_brand", "utm_social",
"fbclid", "gclid", "dclid", "gclsrc", "yclid", "msclkid",
"mc_eid", "mc_cid",
"_ga", "_gl", "ga_source", "ga_medium",
"igshid", "igsh",
"hsa_acc", "hsa_cam", "hsa_grp", "hsa_ad", "hsa_src",
"hsa_tgt", "hsa_kw", "hsa_mt", "hsa_net", "hsa_ver",
"spm", "scm", "vero_id", "vero_conv"
)
fun Uri.stripTrackingParams(): Uri {
val scheme = scheme?.lowercase()
if (scheme != "http" && scheme != "https") {
return this
}
val queryNames = runCatching {
queryParameterNames
}.getOrDefault(emptySet())
if (queryNames.isEmpty()) {
return this
}
val keep = queryNames.filter { paramName ->
paramName.lowercase() !in TRACKING_PARAMS
}
if (keep.size == queryNames.size) {
return this
}
val builder = buildUpon().clearQuery()
for (paramName in keep) {
getQueryParameters(paramName).forEach { paramValue ->
builder.appendQueryParameter(paramName, paramValue)
}
}
return builder.build()
}
@@ -29,6 +29,7 @@ private fun exportCsv(
return
}
val columns = arrayOf(
Database.SCANS_PINNED,
Database.SCANS_DATETIME,
Database.SCANS_FORMAT,
Database.SCANS_NAME,
@@ -3,15 +3,6 @@ package de.markusfisch.android.binaryeye.database
import android.database.Cursor
import de.markusfisch.android.zxingcpp.ZxingCpp
// Overwrite Kotlin's ".use" function for Cursor because Cursor cannot
// be cast to Closeable below API level 16. This should be removed when
// the minSDK is increased to at least JELLY_BEAN (16).
inline fun <R> Cursor.use(block: (Cursor) -> R): R = try {
block.invoke(this)
} finally {
close()
}
fun Cursor.getInt(name: String): Int {
val idx = getColumnIndex(name)
return if (idx < 0) 0 else getInt(idx)
@@ -7,6 +7,8 @@ import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.preference.Preferences
import de.markusfisch.android.binaryeye.zxingcpp.migrateBarcodeFormatName
import de.markusfisch.android.binaryeye.zxingcpp.oldToNewFormatNames
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
class Database {
@@ -19,19 +21,21 @@ class Database {
fun getScans(query: String? = null): Cursor = db.rawQuery(
"""SELECT
$SCANS_ID,
$SCANS_PINNED,
$SCANS_DATETIME,
$SCANS_NAME,
$SCANS_TEXT,
$SCANS_FORMAT
FROM $SCANS
${getWhereClause(query)}
ORDER BY $SCANS_DATETIME DESC
ORDER BY $SCANS_PINNED DESC, $SCANS_DATETIME DESC
""".trimMargin(), getWhereArguments(query)
)
fun getScansDetailed(query: String? = null): Cursor = db.rawQuery(
"""SELECT
$SCANS_ID,
$SCANS_PINNED,
$SCANS_DATETIME,
$SCANS_NAME,
$SCANS_TEXT,
@@ -48,7 +52,7 @@ class Database {
$SCANS_GTIN_ISSUE_NUMBER
FROM $SCANS
${getWhereClause(query)}
ORDER BY $SCANS_DATETIME DESC
ORDER BY $SCANS_PINNED DESC, $SCANS_DATETIME DESC
""".trimMargin(), getWhereArguments(query)
)
@@ -57,7 +61,8 @@ class Database {
prefix: String = "WHERE"
) = if (query?.isNotEmpty() == true) {
"""$prefix $SCANS_TEXT LIKE ?
OR $SCANS_NAME LIKE ?"""
OR $SCANS_NAME LIKE ?
OR $SCANS_FORMAT LIKE ?"""
} else {
""
}
@@ -66,7 +71,7 @@ class Database {
query: String?
) = if (query?.isNotEmpty() == true) {
val instr = "%$query%"
arrayOf(instr, instr)
arrayOf(instr, instr, instr)
} else {
null
}
@@ -74,6 +79,7 @@ class Database {
fun getScansDetailed(ids: LongArray): Cursor = db.rawQuery(
"""SELECT
$SCANS_ID,
$SCANS_PINNED,
$SCANS_DATETIME,
$SCANS_NAME,
$SCANS_TEXT,
@@ -90,13 +96,14 @@ class Database {
$SCANS_GTIN_ISSUE_NUMBER
FROM $SCANS
WHERE $SCANS_ID IN (${ids.joinToString(",")})
ORDER BY $SCANS_DATETIME DESC
ORDER BY $SCANS_PINNED DESC, $SCANS_DATETIME DESC
""".trimMargin(), null
)
fun getScan(id: Long): Scan? = db.rawQuery(
"""SELECT
$SCANS_ID,
$SCANS_PINNED,
$SCANS_DATETIME,
$SCANS_NAME,
$SCANS_TEXT,
@@ -123,7 +130,9 @@ class Database {
Scan(
it.getString(SCANS_TEXT),
it.getBlob(SCANS_RAW),
BarcodeFormat.valueOf(it.getString(SCANS_FORMAT)),
BarcodeFormat.valueOf(
it.getString(SCANS_FORMAT).migrateBarcodeFormatName()
),
it.getString(SCANS_ERROR_CORRECTION_LEVEL),
it.getString(SCANS_VERSION),
it.getInt(SCANS_DATA_MASK),
@@ -142,6 +151,7 @@ class Database {
it.getString(SCANS_DATETIME),
it.getLong(SCANS_ID),
it.getString(SCANS_NAME),
it.getInt(SCANS_PINNED) != 0
)
} else {
null
@@ -197,6 +207,7 @@ class Database {
scan.addOn?.let { put(SCANS_GTIN_ADD_ON, it) }
scan.price?.let { put(SCANS_GTIN_PRICE, it) }
scan.issueNumber?.let { put(SCANS_GTIN_ISSUE_NUMBER, it) }
put(SCANS_PINNED, if (scan.pinned) 1 else 0)
}
)
}
@@ -262,8 +273,14 @@ class Database {
db.update(SCANS, cv, "$SCANS_ID = ?", arrayOf("$id"))
}
fun setPinned(id: Long, pinned: Boolean) {
val cv = ContentValues()
cv.put(SCANS_PINNED, if (pinned) 1 else 0)
db.update(SCANS, cv, "$SCANS_ID = ?", arrayOf("$id"))
}
private class OpenHelper(context: Context) :
SQLiteOpenHelper(context, FILE_NAME, null, 8) {
SQLiteOpenHelper(context, FILE_NAME, null, 10) {
override fun onCreate(db: SQLiteDatabase) {
db.createScans()
}
@@ -294,6 +311,12 @@ class Database {
if (oldVersion < 8) {
db.addSymbolColumns()
}
if (oldVersion < 9) {
db.migrateToNativeFormatNames()
}
if (oldVersion < 10) {
db.addPinnedColumn()
}
}
}
@@ -327,6 +350,7 @@ class Database {
const val SCANS_GTIN_ADD_ON = "gtin_add_on"
const val SCANS_GTIN_PRICE = "gtin_price"
const val SCANS_GTIN_ISSUE_NUMBER = "gtin_issue_number"
const val SCANS_PINNED = "pinned"
private fun SQLiteDatabase.createScans() {
execSQL("DROP TABLE IF EXISTS $SCANS".trimMargin())
@@ -350,7 +374,8 @@ class Database {
$SCANS_GTIN_COUNTRY TEXT,
$SCANS_GTIN_ADD_ON TEXT,
$SCANS_GTIN_PRICE TEXT,
$SCANS_GTIN_ISSUE_NUMBER TEXT
$SCANS_GTIN_ISSUE_NUMBER TEXT,
$SCANS_PINNED INTEGER NOT NULL DEFAULT 0
)""".trimMargin()
)
}
@@ -391,6 +416,15 @@ class Database {
execSQL("UPDATE $SCANS SET $SCANS_GTIN_ISSUE_NUMBER = $SCANS_ISSUE_NUMBER")
}
private fun SQLiteDatabase.migrateToNativeFormatNames() {
for ((oldName, newName) in oldToNewFormatNames) {
execSQL(
"UPDATE $SCANS SET $SCANS_FORMAT = ? WHERE $SCANS_FORMAT = ?",
arrayOf(newName, oldName)
)
}
}
private fun SQLiteDatabase.migrateToVersionString() {
execSQL("ALTER TABLE $SCANS ADD $SCANS_VERSION TEXT")
execSQL("UPDATE $SCANS SET $SCANS_VERSION = $SCANS_VERSION_NUMBER")
@@ -406,5 +440,11 @@ class Database {
execSQL("ALTER TABLE $SCANS ADD $SCANS_SYMBOL_HEIGHT INTEGER")
execSQL("ALTER TABLE $SCANS ADD $SCANS_SYMBOL_DATA BLOB")
}
private fun SQLiteDatabase.addPinnedColumn() {
execSQL(
"ALTER TABLE $SCANS ADD COLUMN $SCANS_PINNED INTEGER NOT NULL DEFAULT 0"
)
}
}
}
@@ -21,6 +21,7 @@ fun Cursor.exportJson(): String? {
return null
}
val columns = arrayOf(
Database.SCANS_PINNED,
Database.SCANS_DATETIME,
Database.SCANS_FORMAT,
Database.SCANS_NAME,
@@ -1,9 +1,8 @@
package de.markusfisch.android.binaryeye.database
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import android.os.Bundle
import android.text.format.DateFormat
import de.markusfisch.android.binaryeye.zxingcpp.migrateBarcodeFormatName
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
import de.markusfisch.android.zxingcpp.ZxingCpp.BitMatrix
import de.markusfisch.android.zxingcpp.ZxingCpp.ContentType
@@ -27,8 +26,9 @@ data class Scan(
val issueNumber: String? = null,
val dateTime: String = getDateTime(),
var id: Long = 0L,
val label: String? = null
) : Parcelable {
val label: String? = null,
val pinned: Boolean = false
) {
// Needs to be overwritten manually, as ByteArray is an array and
// this isn't handled well by Kotlin.
override fun equals(other: Any?): Boolean {
@@ -57,7 +57,9 @@ data class Scan(
country == other.country &&
addOn == other.addOn &&
price == other.price &&
issueNumber == other.issueNumber
issueNumber == other.issueNumber &&
label == other.label &&
pinned == other.pinned
}
// Needs to be overwritten manually, as ByteArray is an array and
@@ -78,60 +80,11 @@ data class Scan(
result = 31 * result + (addOn?.hashCode() ?: 0)
result = 31 * result + (price?.hashCode() ?: 0)
result = 31 * result + (issueNumber?.hashCode() ?: 0)
result = 31 * result + (label?.hashCode() ?: 0)
result = 31 * result + pinned.hashCode()
return result
}
private constructor(parcel: Parcel) : this(
text = parcel.readString() ?: "",
raw = parcel.readSizedByteArray(),
format = BarcodeFormat.valueOf(parcel.readString() ?: ""),
errorCorrectionLevel = parcel.readString(),
version = parcel.readString(),
dataMask = parcel.readInt(),
symbol = parcel.readBitMatrix(),
sequenceSize = parcel.readInt(),
sequenceIndex = parcel.readInt(),
sequenceId = parcel.readString() ?: "",
country = parcel.readString(),
addOn = parcel.readString(),
price = parcel.readString(),
issueNumber = parcel.readString(),
dateTime = parcel.readString() ?: "",
id = parcel.readLong(),
label = parcel.readString()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.apply {
writeString(text)
writeSizedByteArray(raw)
writeString(format.name)
writeString(errorCorrectionLevel)
writeString(version)
writeInt(dataMask)
writeBitMatrix(symbol)
writeInt(sequenceSize)
writeInt(sequenceIndex)
writeString(sequenceId)
writeString(country)
writeString(addOn)
writeString(price)
writeString(issueNumber)
writeString(dateTime)
writeLong(id)
writeString(label)
}
}
override fun describeContents() = 0
companion object {
@JvmField
val CREATOR = object : Parcelable.Creator<Scan> {
override fun createFromParcel(parcel: Parcel) = Scan(parcel)
override fun newArray(size: Int) = arrayOfNulls<Scan>(size)
}
}
}
fun Result.toScan(): Scan {
@@ -159,16 +112,64 @@ fun Result.toScan(): Scan {
)
}
fun Scan.toBundle() = Bundle().apply {
putString(SCAN_TEXT, text)
putByteArray(SCAN_RAW, raw)
putString(SCAN_FORMAT, format.name)
putString(SCAN_ERROR_CORRECTION_LEVEL, errorCorrectionLevel)
putString(SCAN_VERSION, version)
putInt(SCAN_DATA_MASK, dataMask)
putInt(SCAN_SYMBOL_WIDTH, symbol?.width ?: 0)
putInt(SCAN_SYMBOL_HEIGHT, symbol?.height ?: 0)
putByteArray(SCAN_SYMBOL_DATA, symbol?.data)
putInt(SCAN_SEQUENCE_SIZE, sequenceSize)
putInt(SCAN_SEQUENCE_INDEX, sequenceIndex)
putString(SCAN_SEQUENCE_ID, sequenceId)
putString(SCAN_COUNTRY, country)
putString(SCAN_ADD_ON, addOn)
putString(SCAN_PRICE, price)
putString(SCAN_ISSUE_NUMBER, issueNumber)
putString(SCAN_DATE_TIME, dateTime)
putLong(SCAN_ID, id)
putString(SCAN_LABEL, label)
putBoolean(SCAN_PINNED, pinned)
}
fun Bundle.toScan(): Scan? {
val formatName = getString(SCAN_FORMAT)?.migrateBarcodeFormatName()
if (formatName.isNullOrEmpty()) {
return null
}
return try {
Scan(
text = getString(SCAN_TEXT) ?: "",
raw = getByteArray(SCAN_RAW),
format = BarcodeFormat.valueOf(formatName),
errorCorrectionLevel = getString(SCAN_ERROR_CORRECTION_LEVEL),
version = getString(SCAN_VERSION),
dataMask = getInt(SCAN_DATA_MASK, -1),
symbol = getBitMatrix(),
sequenceSize = getInt(SCAN_SEQUENCE_SIZE, -1),
sequenceIndex = getInt(SCAN_SEQUENCE_INDEX, -1),
sequenceId = getString(SCAN_SEQUENCE_ID) ?: "",
country = getString(SCAN_COUNTRY),
addOn = getString(SCAN_ADD_ON),
price = getString(SCAN_PRICE),
issueNumber = getString(SCAN_ISSUE_NUMBER),
dateTime = getString(SCAN_DATE_TIME) ?: getDateTime(),
id = getLong(SCAN_ID, 0L),
label = getString(SCAN_LABEL),
pinned = getBoolean(SCAN_PINNED, false)
)
} catch (_: IllegalArgumentException) {
null
}
}
private fun getDateTime(
time: Long = System.currentTimeMillis()
) = DateFormat.format(
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
// Up to API level 17, 'k' was interpreted as 'H', which wasn't
// implemented until API level 18.
"yyyy-MM-dd kk:mm:ss"
} else {
"yyyy-MM-dd HH:mm:ss"
},
"yyyy-MM-dd HH:mm:ss",
time
).toString() + String.format(
Locale.getDefault(),
@@ -176,39 +177,33 @@ private fun getDateTime(
time % 1000
)
private fun Parcel.writeSizedByteArray(array: ByteArray?) {
val size = array?.size ?: 0
writeInt(size)
if (size > 0) {
writeByteArray(array)
}
}
private fun Parcel.readSizedByteArray(): ByteArray? {
val size = readInt()
return if (size > 0) {
val array = ByteArray(size)
readByteArray(array)
array
} else {
null
}
}
private fun Parcel.writeBitMatrix(bm: BitMatrix?) {
writeInt(bm?.width ?: 0)
bm?.let {
writeInt(bm.height)
writeSizedByteArray(bm.data)
}
}
private fun Parcel.readBitMatrix(): BitMatrix? {
val width = readInt()
if (width < 1) {
private fun Bundle.getBitMatrix(): BitMatrix? {
val width = getInt(SCAN_SYMBOL_WIDTH, 0)
val height = getInt(SCAN_SYMBOL_HEIGHT, 0)
val data = getByteArray(SCAN_SYMBOL_DATA)
if (width < 1 || height < 1 || data == null) {
return null
}
val height = readInt()
val data = readSizedByteArray() ?: return null
return BitMatrix(width, height, data)
}
private const val SCAN_TEXT = "text"
private const val SCAN_RAW = "raw"
private const val SCAN_FORMAT = "format"
private const val SCAN_ERROR_CORRECTION_LEVEL = "error_correction_level"
private const val SCAN_VERSION = "version"
private const val SCAN_DATA_MASK = "data_mask"
private const val SCAN_SYMBOL_WIDTH = "symbol_width"
private const val SCAN_SYMBOL_HEIGHT = "symbol_height"
private const val SCAN_SYMBOL_DATA = "symbol_data"
private const val SCAN_SEQUENCE_SIZE = "sequence_size"
private const val SCAN_SEQUENCE_INDEX = "sequence_index"
private const val SCAN_SEQUENCE_ID = "sequence_id"
private const val SCAN_COUNTRY = "country"
private const val SCAN_ADD_ON = "add_on"
private const val SCAN_PRICE = "price"
private const val SCAN_ISSUE_NUMBER = "issue_number"
private const val SCAN_DATE_TIME = "date_time"
private const val SCAN_ID = "id"
private const val SCAN_LABEL = "label"
private const val SCAN_PINNED = "pinned"
@@ -1,7 +1,6 @@
package de.markusfisch.android.binaryeye.fragment
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
@@ -9,15 +8,20 @@ import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.net.wifi.WifiManager
import android.os.Build
import android.os.Bundle
import android.support.annotation.RequiresApi
import android.support.v14.preference.MultiSelectListPreference
import android.support.v7.preference.ListPreference
import android.support.v7.preference.Preference
import android.support.v7.preference.PreferenceFragmentCompat
import android.support.v7.preference.PreferenceGroup
import androidx.annotation.RequiresApi
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceGroup
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.activity.AutomatedActionsActivity
import de.markusfisch.android.binaryeye.activity.IgnoreCodesActivity
import de.markusfisch.android.binaryeye.activity.NetworkSuggestionsActivity
import de.markusfisch.android.binaryeye.activity.ProfilesActivity
import de.markusfisch.android.binaryeye.activity.SplashActivity
import de.markusfisch.android.binaryeye.app.addFragment
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
import de.markusfisch.android.binaryeye.app.hasBluetoothPermission
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.bluetooth.setBluetoothHosts
@@ -36,17 +40,19 @@ class PreferencesFragment : PreferenceFragmentCompat() {
key: String?
) {
key ?: return
val preference = findPreference(key) ?: return
val preference = findPreference<Preference>(key) ?: return
if (preference.key != PROFILE) {
prefs.update()
}
when (preference.key) {
"custom_locale" -> {
"custom_locale",
"dynamic_colors" -> {
activity?.restartApp()
return
}
"beep_tone_name" -> beepConfirm()
"beep_tone_name",
"beep_stream_name" -> beepConfirm()
"send_scan_bluetooth" -> if (
prefs.sendScanBluetooth &&
@@ -76,33 +82,48 @@ class PreferencesFragment : PreferenceFragmentCompat() {
preferenceScreen = null
setPreferencesFromResource(R.xml.preferences, rootKey)
preferenceScreen.sharedPreferences
.registerOnSharedPreferenceChangeListener(changeListener)
?.registerOnSharedPreferenceChangeListener(changeListener)
setSummaries(preferenceScreen)
setIcons()
wireProfiles()
wireAutomatedActions()
wireIgnoreCodes()
setBluetoothResources()
wireClearNetworkPreferences()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
findPreference<PreferenceGroup>("appearance_category")?.isVisible = false
}
}
private fun wireProfiles() {
findPreference(PROFILE).apply {
findPreference<Preference>(PROFILE)?.apply {
updateProfileSummary(this)
onPreferenceClickListener = Preference.OnPreferenceClickListener {
activity?.supportFragmentManager?.addFragment(
ProfilesFragment()
)
startActivity(Intent(activity, ProfilesActivity::class.java))
true
}
}
}
private fun wireAutomatedActions() {
findPreference(AUTOMATED_ACTIONS).apply {
findPreference<Preference>(AUTOMATED_ACTIONS)?.apply {
updateAutomatedActionsSummary(this)
onPreferenceClickListener = Preference.OnPreferenceClickListener {
activity?.supportFragmentManager?.addFragment(
AutomatedActionsFragment()
startActivity(
Intent(activity, AutomatedActionsActivity::class.java)
)
true
}
}
}
private fun wireIgnoreCodes() {
findPreference<Preference>(IGNORE_CODES)?.apply {
updateIgnoreCodesSummary(this)
onPreferenceClickListener = Preference.OnPreferenceClickListener {
startActivity(
Intent(activity, IgnoreCodesActivity::class.java)
)
true
}
@@ -113,20 +134,20 @@ class PreferencesFragment : PreferenceFragmentCompat() {
if (prefs.sendScanBluetooth &&
activity?.hasBluetoothPermission() == true
) {
setBluetoothHosts(
findPreference("send_scan_bluetooth_host") as ListPreference
)
findPreference<ListPreference>("send_scan_bluetooth_host")?.let {
setBluetoothHosts(it)
}
}
}
private fun wireClearNetworkPreferences() {
findPreference("clear_network_suggestions").apply {
findPreference<Preference>("clear_network_suggestions")?.apply {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
// From R+ we can query past network suggestions and
// make them editable.
setOnPreferenceClickListener {
activity?.supportFragmentManager?.addFragment(
NetworkSuggestionsFragment()
startActivity(
Intent(activity, NetworkSuggestionsActivity::class.java)
)
true
}
@@ -136,7 +157,7 @@ class PreferencesFragment : PreferenceFragmentCompat() {
// adding network suggestions on Q as well, so we
// need to keep this option.
setOnPreferenceClickListener {
context.askToClearNetworkSuggestions()
context?.askToClearNetworkSuggestions()
true
}
} else {
@@ -148,7 +169,7 @@ class PreferencesFragment : PreferenceFragmentCompat() {
@RequiresApi(Build.VERSION_CODES.Q)
private fun Context.askToClearNetworkSuggestions() {
AlertDialog.Builder(this)
MaterialAlertDialogBuilder(this)
.setMessage(R.string.really_remove_all_networks)
.setPositiveButton(android.R.string.ok) { _, _ ->
clearNetworkSuggestions()
@@ -176,10 +197,13 @@ class PreferencesFragment : PreferenceFragmentCompat() {
loadPreferences()
}
activity?.setTitle(R.string.preferences)
findPreference(AUTOMATED_ACTIONS)?.let {
findPreference<Preference>(AUTOMATED_ACTIONS)?.let {
updateAutomatedActionsSummary(it)
}
findPreference(PROFILE)?.let {
findPreference<Preference>(IGNORE_CODES)?.let {
updateIgnoreCodesSummary(it)
}
findPreference<Preference>(PROFILE)?.let {
updateProfileSummary(it)
}
listView.setPaddingFromWindowInsets()
@@ -190,12 +214,12 @@ class PreferencesFragment : PreferenceFragmentCompat() {
override fun onPause() {
super.onPause()
preferenceScreen.sharedPreferences
.unregisterOnSharedPreferenceChangeListener(changeListener)
?.unregisterOnSharedPreferenceChangeListener(changeListener)
}
override fun onDisplayPreferenceDialog(preference: Preference) {
if (preference is UrlPreference) {
val fm = fragmentManager
val fm = parentFragmentManager
UrlDialogFragment.newInstance(preference.key).apply {
setTargetFragment(this@PreferencesFragment, 0)
show(fm, null)
@@ -219,31 +243,82 @@ class PreferencesFragment : PreferenceFragmentCompat() {
}
private fun setSummary(preference: Preference) {
if (preference.key == AUTOMATED_ACTIONS) {
updateAutomatedActionsSummary(preference)
return
when (preference.key) {
AUTOMATED_ACTIONS -> return updateAutomatedActionsSummary(preference)
IGNORE_CODES -> return updateIgnoreCodesSummary(preference)
}
when (preference) {
is UrlPreference -> {
preference.summary = preference.getUrl()
}
is UrlPreference -> preference.summary = preference.getUrl()
is ListPreference -> preference.setSummary(preference.entry)
is MultiSelectListPreference -> preference.summary =
getSortedSummary(preference)
is ListPreference -> {
preference.setSummary(preference.entry)
}
is MultiSelectListPreference -> {
preference.summary = preference.values.joinToString(", ") {
it.replace(Regex("_"), " ")
}
}
is PreferenceGroup -> {
setSummaries(preference)
}
is PreferenceGroup -> setSummaries(preference)
}
}
private fun getSortedSummary(preference: MultiSelectListPreference): String {
val labelsByValue = mutableMapOf<String, String>()
val entries = preference.entries
val entryValues = preference.entryValues
val count = minOf(entries.size, entryValues.size)
for (i in 0 until count) {
labelsByValue[entryValues[i].toString()] = entries[i].toString()
}
return preference.values
.map { value ->
labelsByValue[value] ?: value.prettifyFormatName()
}
.sortedBy { it.lowercase() }
.joinToString(", ")
}
private fun setIcons() {
setIcon(PROFILE, R.drawable.ic_action_profile)
setIcon("formats", R.drawable.ic_action_scan)
setIcon("show_crop_handle", R.drawable.ic_action_crop)
setIcon("show_crosshairs", R.drawable.ic_action_search)
setIcon("zoom_by_swiping", R.drawable.ic_action_search)
setIcon("auto_rotate", R.drawable.ic_action_down)
setIcon("try_harder", R.drawable.ic_action_search)
setIcon("bulk_mode", R.drawable.ic_action_bulk_mode)
setIcon("bulk_mode_delay", R.drawable.ic_action_delay)
setIcon("show_toast_in_bulk_mode", R.drawable.ic_action_toast)
setIcon("vibrate", R.drawable.ic_action_vibrate)
setIcon("beep", R.drawable.ic_action_beep)
setIcon("beep_tone_name", R.drawable.ic_action_notification_sound)
setIcon("beep_stream_name", R.drawable.ic_action_notification_sound)
setIcon("use_history", R.drawable.ic_action_history)
setIcon("ignore_duplicates_name", R.drawable.ic_action_remove)
setIcon(IGNORE_CODES, R.drawable.ic_action_remove)
setIcon("copy_immediately", R.drawable.ic_action_copy)
setIcon("send_scan_active", R.drawable.ic_action_forward)
setIcon("send_scan_url", R.drawable.ic_action_link)
setIcon("send_scan_type", R.drawable.ic_action_link)
setIcon("send_scan_bluetooth", R.drawable.ic_action_bluetooth)
setIcon("send_scan_bluetooth_host", R.drawable.ic_action_bluetooth)
setIcon("open_immediately", R.drawable.ic_action_open)
setIcon("strip_tracking_params", R.drawable.ic_action_remove)
setIcon("show_meta_data", R.drawable.ic_action_info)
setIcon("show_hex_dump", R.drawable.ic_action_info)
setIcon("show_checksum", R.drawable.ic_action_info)
setIcon("show_recreation", R.drawable.ic_action_scan)
setIcon("close_automatically", R.drawable.ic_action_back)
setIcon("default_search_url", R.drawable.ic_action_search)
setIcon("open_with_url", R.drawable.ic_action_link)
setIcon(AUTOMATED_ACTIONS, R.drawable.ic_action_create)
setIcon("clear_network_suggestions", R.drawable.ic_action_wifi)
setIcon("brighten_screen", R.drawable.ic_action_bright)
setIcon("custom_locale", R.drawable.ic_action_preferences)
setIcon("dynamic_colors", R.drawable.ic_action_preferences)
}
private fun setIcon(key: String, iconResId: Int) {
findPreference<Preference>(key)?.setIcon(iconResId)
}
private fun updateAutomatedActionsSummary(preference: Preference) {
val count = prefs.automatedActions.size
preference.summary = if (count == 0) {
@@ -257,25 +332,37 @@ class PreferencesFragment : PreferenceFragmentCompat() {
}
}
private fun updateIgnoreCodesSummary(preference: Preference) {
val count = prefs.ignoreCodes.size
preference.summary = if (count == 0) {
getString(R.string.ignore_codes_none)
} else {
resources.getQuantityString(
R.plurals.ignore_codes_count,
count,
count
)
}
}
private fun updateProfileSummary(preference: Preference) {
preference.summary = prefs.profile ?: getString(R.string.profile_default)
preference.summary = prefs.profileLabel(requireContext(), prefs.profile)
lastProfile = prefs.profile
}
companion object {
private const val PROFILE = "profile"
private const val AUTOMATED_ACTIONS = "automated_actions"
private const val IGNORE_CODES = "ignore_codes"
}
}
private fun Activity.restartApp() {
val intent = Intent(this, SplashActivity::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
intent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK
)
}
intent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK
)
startActivity(intent)
finish()
// Restart to begin with an unmodified Locale to follow system settings.
@@ -1,9 +1,9 @@
package de.markusfisch.android.binaryeye.fragment
import android.os.Bundle
import android.support.v7.preference.PreferenceDialogFragmentCompat
import android.view.View
import android.widget.TextView
import androidx.preference.PreferenceDialogFragmentCompat
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.database.Scan
@@ -15,10 +15,10 @@ class UrlDialogFragment : PreferenceDialogFragmentCompat() {
private var urlView: TextView? = null
private var testButton: TextView? = null
override fun onBindDialogView(view: View?) {
override fun onBindDialogView(view: View) {
super.onBindDialogView(view)
urlView = view?.findViewById(R.id.url)
testButton = view?.findViewById(R.id.test_url)
urlView = view.findViewById(R.id.url)
testButton = view.findViewById(R.id.test_url)
testButton?.setOnClickListener {
testUrl(testButton)
}
@@ -51,7 +51,7 @@ class UrlDialogFragment : PreferenceDialogFragmentCompat() {
Scan(
"test",
null,
BarcodeFormat.NONE
BarcodeFormat.None
).sendAsync(url, prefs.sendScanType) { code, body ->
textView.text = when {
code != null -> "$code"
@@ -3,12 +3,12 @@ package de.markusfisch.android.binaryeye.graphics
import android.content.Context
import android.graphics.DashPathEffect
import android.graphics.Paint
import android.support.v4.content.ContextCompat
import androidx.core.content.ContextCompat
import de.markusfisch.android.binaryeye.R
fun Context.getDashedBorderPaint() = Paint(Paint.ANTI_ALIAS_FLAG).apply {
val dp = resources.displayMetrics.density
color = ContextCompat.getColor(applicationContext, R.color.crop_bound)
color = ContextCompat.getColor(this@getDashedBorderPaint, R.color.crop_bound)
style = Paint.Style.STROKE
strokeWidth = dp * 2f
pathEffect = DashPathEffect(floatArrayOf(10f * dp, 10f * dp), 0f)
@@ -1,30 +1,32 @@
package de.markusfisch.android.binaryeye.graphics
import android.content.res.Resources
import android.content.res.Resources.Theme
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.os.Build
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.createBitmap
fun Resources.getBitmapFromDrawable(
resId: Int
): Bitmap = getBitmapFromDrawable(getDrawableCompat(resId))
): Bitmap = getBitmapFromDrawable(resId, null)
fun Resources.getDrawableCompat(
resId: Int
): Drawable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getDrawable(resId, null)
} else {
@Suppress("DEPRECATION")
getDrawable(resId)
}
fun Resources.getBitmapFromDrawable(
resId: Int,
theme: Theme?
): Bitmap = getBitmapFromDrawable(
ResourcesCompat.getDrawable(this, resId, theme) ?: throw Resources.NotFoundException(
"Drawable resource ID #0x${resId.toString(16)}"
)
)
private fun getBitmapFromDrawable(drawable: Drawable): Bitmap {
if (drawable is BitmapDrawable) {
return drawable.bitmap
}
val bitmap = Bitmap.createBitmap(
val bitmap = createBitmap(
drawable.intrinsicWidth,
drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
@@ -40,6 +40,15 @@ fun Rect.setFrameRoi(
frameRoiF.bottom.roundToInt()
)
}
val clampedLeft = left.coerceIn(0, frameMetrics.width)
val clampedTop = top.coerceIn(0, frameMetrics.height)
val clampedRight = right.coerceIn(0, frameMetrics.width)
val clampedBottom = bottom.coerceIn(0, frameMetrics.height)
if (clampedLeft >= clampedRight || clampedTop >= clampedBottom) {
setEmpty()
} else {
set(clampedLeft, clampedTop, clampedRight, clampedBottom)
}
}
fun Matrix.setFrameToView(
@@ -88,3 +97,57 @@ fun Matrix.mapPosition(
mapPoints(coords)
return i
}
fun Rect.setCovered(
viewWidth: Int,
viewHeight: Int,
frameMetrics: FrameMetrics
) {
val frameWidth: Int
val frameHeight: Int
when (frameMetrics.orientation) {
90, 270 -> {
frameWidth = frameMetrics.height
frameHeight = frameMetrics.width
}
else -> {
frameWidth = frameMetrics.width
frameHeight = frameMetrics.height
}
}
if (frameWidth < 1 || frameHeight < 1) {
set(0, 0, 0, 0)
return
}
var coveredWidth = frameWidth
var coveredHeight = frameHeight
if (viewWidth.toLong() * coveredWidth <
viewHeight.toLong() * coveredHeight
) {
coveredWidth = coveredWidth * viewHeight / coveredHeight
coveredHeight = viewHeight
} else {
coveredHeight = coveredHeight * viewWidth / coveredWidth
coveredWidth = viewWidth
}
val left = (viewWidth - coveredWidth) / 2
val top = (viewHeight - coveredHeight) / 2
set(
left,
top,
left + coveredWidth,
top + coveredHeight
)
}
fun Matrix.mapViewYToFrame(viewY: Float): Float {
val values = FloatArray(9)
getValues(values)
val scaleY = values[Matrix.MSCALE_Y]
return if (scaleY != 0f) {
(viewY - values[Matrix.MTRANS_Y]) / scaleY
} else {
viewY
}
}
@@ -2,12 +2,13 @@ package de.markusfisch.android.binaryeye.graphics
import android.graphics.Bitmap
import android.graphics.Canvas
import androidx.core.graphics.createBitmap
fun Bitmap.fixTransparency(): Bitmap {
val result = analyzeTransparency()
val cfg = config
return if (cfg != null && result.hasTransparentPixels) {
val copy = Bitmap.createBitmap(
val copy = createBitmap(
width,
height,
cfg
@@ -6,8 +6,8 @@ import android.content.Context
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.support.annotation.MainThread
import android.widget.EditText
import androidx.annotation.MainThread
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.alertDialog
import java.io.File
@@ -5,13 +5,20 @@ import android.media.ToneGenerator
import de.markusfisch.android.binaryeye.app.prefs
private var confirmToneGenerator: ToneGenerator? = null
private var confirmToneStream = AudioManager.STREAM_MUSIC
private var errorToneGenerator: ToneGenerator? = null
fun beepConfirm() {
val tg = confirmToneGenerator ?: ToneGenerator(
AudioManager.STREAM_NOTIFICATION,
ToneGenerator.MAX_VOLUME
)
val stream = prefs.beepStream()
val tg = if (confirmToneGenerator == null || confirmToneStream != stream) {
confirmToneGenerator?.release()
ToneGenerator(stream, ToneGenerator.MAX_VOLUME).also {
confirmToneGenerator = it
confirmToneStream = stream
}
} else {
confirmToneGenerator!!
}
confirmToneGenerator = tg
tg.startTone(prefs.beepTone())
}
@@ -28,6 +35,7 @@ fun beepError() {
fun releaseToneGenerators() {
confirmToneGenerator?.release()
confirmToneGenerator = null
confirmToneStream = AudioManager.STREAM_MUSIC
errorToneGenerator?.release()
errorToneGenerator = null
}
@@ -2,6 +2,7 @@ package de.markusfisch.android.binaryeye.preference
import android.content.SharedPreferences
import android.graphics.Point
import androidx.core.content.edit
import org.json.JSONObject
fun SharedPreferences.storeCropHandle(
@@ -10,13 +11,15 @@ fun SharedPreferences.storeCropHandle(
viewHeight: Int,
cropHandle: Point
) {
edit().putString(
cropHandleKey(name, viewWidth, viewHeight),
JSONObject().apply {
put("x", cropHandle.x)
put("y", cropHandle.y)
}.toString()
).apply()
edit {
putString(
cropHandleKey(name, viewWidth, viewHeight),
JSONObject().apply {
put("x", cropHandle.x)
put("y", cropHandle.y)
}.toString()
)
}
}
fun SharedPreferences.restoreCropHandle(
@@ -0,0 +1,75 @@
package de.markusfisch.android.binaryeye.preference
import org.json.JSONArray
import org.json.JSONObject
data class IgnoreCode(
val pattern: String,
val format: String? = null
) {
private val compiledRegex: Regex? by lazy {
try {
Regex(pattern)
} catch (_: Exception) {
null
}
}
fun matches(content: String, format: String): Boolean =
(this.format == null || this.format == format) &&
compiledRegex?.containsMatchIn(content) == true
fun toJson(): JSONObject = JSONObject().apply {
put(KEY_PATTERN, pattern)
put(KEY_FORMAT, format ?: "")
}
companion object {
private const val KEY_PATTERN = "pattern"
private const val KEY_FORMAT = "format"
fun fromJson(obj: JSONObject): IgnoreCode? {
val pattern = obj.optString(KEY_PATTERN, "").trim()
if (pattern.isEmpty()) {
return null
}
val format = obj.optString(KEY_FORMAT, "").trim()
return IgnoreCode(
pattern = pattern,
format = format.ifEmpty { null }
)
}
fun fromJsonArray(json: String): MutableList<IgnoreCode> {
val items = ArrayList<IgnoreCode>()
val array = try {
JSONArray(json)
} catch (_: Exception) {
JSONArray()
}
for (i in 0 until array.length()) {
when (val item = array.opt(i)) {
is String -> {
val pattern = item.trim()
if (pattern.isNotEmpty()) {
items.add(IgnoreCode(pattern))
}
}
is JSONObject -> {
fromJson(item)?.let { items.add(it) }
}
}
}
return items
}
fun toJsonArray(items: List<IgnoreCode>): String {
val array = JSONArray()
items.forEach { item ->
array.put(item.toJson())
}
return array.toString()
}
}
}
@@ -5,14 +5,21 @@ import android.content.Context
import android.content.SharedPreferences
import android.graphics.Point
import android.media.ToneGenerator
import android.os.Build
import android.preference.PreferenceManager
import android.support.annotation.RequiresApi
import androidx.core.content.edit
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.automation.AutomatedAction
import de.markusfisch.android.binaryeye.automation.AutomatedAction.Companion.fromJsonArray
import de.markusfisch.android.binaryeye.automation.AutomatedAction.Companion.toJsonArray
import de.markusfisch.android.binaryeye.zxingcpp.migrateBarcodeFormatName
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
import org.json.JSONArray
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import de.markusfisch.android.binaryeye.preference.IgnoreCode.Companion.fromJsonArray as ignoreCodesFromJsonArray
import de.markusfisch.android.binaryeye.preference.IgnoreCode.Companion.toJsonArray as ignoreCodesToJsonArray
class Preferences {
lateinit var defaultPreferences: SharedPreferences
@@ -22,31 +29,42 @@ class Preferences {
var profile: String? = null
set(value) {
defaultPreferences.edit().putString(PROFILE, value).apply()
defaultPreferences.edit {
putString(PROFILE, value)
}
field = value
}
var barcodeFormats = setOf(
BarcodeFormat.AZTEC.name,
BarcodeFormat.CODABAR.name,
BarcodeFormat.CODE_39.name,
BarcodeFormat.CODE_93.name,
BarcodeFormat.CODE_128.name,
BarcodeFormat.DATA_BAR.name,
BarcodeFormat.DATA_BAR_EXPANDED.name,
BarcodeFormat.DATA_MATRIX.name,
BarcodeFormat.DX_FILM_EDGE.name,
BarcodeFormat.EAN_8.name,
BarcodeFormat.EAN_13.name,
BarcodeFormat.Aztec.name,
BarcodeFormat.Codabar.name,
BarcodeFormat.Code39Std.name,
BarcodeFormat.Code39Ext.name,
BarcodeFormat.Code32.name,
BarcodeFormat.PZN.name,
BarcodeFormat.Code93.name,
BarcodeFormat.Code128.name,
BarcodeFormat.DataBar.name,
BarcodeFormat.DataBarOmni.name,
BarcodeFormat.DataBarStk.name,
BarcodeFormat.DataBarLtd.name,
BarcodeFormat.DataBarExp.name,
BarcodeFormat.DataBarExpStk.name,
BarcodeFormat.DataMatrix.name,
BarcodeFormat.DXFilmEdge.name,
BarcodeFormat.EAN8.name,
BarcodeFormat.EAN13.name,
BarcodeFormat.ITF.name,
BarcodeFormat.MAXICODE.name,
BarcodeFormat.PDF_417.name,
BarcodeFormat.QR_CODE.name,
BarcodeFormat.MICRO_QR_CODE.name,
BarcodeFormat.RMQR_CODE.name,
BarcodeFormat.UPC_A.name,
BarcodeFormat.UPC_E.name,
BarcodeFormat.MaxiCode.name,
BarcodeFormat.PDF417.name,
BarcodeFormat.Telepen.name,
BarcodeFormat.TelepenAlpha.name,
BarcodeFormat.TelepenNumeric.name,
BarcodeFormat.QRCode.name,
BarcodeFormat.MicroQRCode.name,
BarcodeFormat.RMQRCode.name,
BarcodeFormat.UPCA.name,
BarcodeFormat.UPCE.name,
)
@RequiresApi(Build.VERSION_CODES.HONEYCOMB)
set(value) {
apply(BARCODE_FORMATS, value)
field = value
@@ -56,6 +74,11 @@ class Preferences {
apply(SHOW_CROP_HANDLE, value)
field = value
}
var showCrosshairs = false
set(value) {
apply(SHOW_CROSSHAIRS, value)
field = value
}
var zoomBySwiping = true
set(value) {
apply(ZOOM_BY_SWIPING, value)
@@ -101,6 +124,11 @@ class Preferences {
apply(BEEP_TONE_NAME, value)
field = value
}
var beepStreamName = "stream_music"
set(value) {
apply(BEEP_STREAM_NAME, value)
field = value
}
var useHistory = false
set(value) {
apply(USE_HISTORY, value)
@@ -111,6 +139,8 @@ class Preferences {
apply(IGNORE_DUPLICATES_NAME, value)
field = value
}
var ignoreCodes = mutableListOf(IgnoreCode(DEFAULT_IGNORE_CODE_PATTERN))
private set
var copyImmediately = false
set(value) {
apply(COPY_IMMEDIATELY, value)
@@ -121,6 +151,11 @@ class Preferences {
apply(OPEN_IMMEDIATELY, value)
field = value
}
var stripTrackingParams = true
set(value) {
apply(STRIP_TRACKING_PARAMS, value)
field = value
}
var showMetaData = true
set(value) {
apply(SHOW_META_DATA, value)
@@ -223,6 +258,11 @@ class Preferences {
apply(PREVIEW_SCALE, value)
field = value
}
var dynamicColors = true
set(value) {
apply(DYNAMIC_COLORS, value)
field = value
}
var automatedActions = mutableListOf<AutomatedAction>()
private set
@@ -233,6 +273,7 @@ class Preferences {
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context)
loadProfiles()
addPredefinedProfiles(context)
load(context, defaultPreferences.getString(PROFILE, null))
}
@@ -270,36 +311,230 @@ class Preferences {
}
fun saveProfiles() {
defaultPreferences.edit().putString(
PROFILES,
JSONArray(profiles).toString()
).apply()
defaultPreferences.edit {
putString(PROFILES, org.json.JSONArray(profiles).toString())
}
}
fun loadProfiles() {
profiles.clear()
JSONArray(
org.json.JSONArray(
defaultPreferences.getString(PROFILES, "[]")
).let {
profiles.addAll(Array(it.length()) { i -> it.getString(i) })
}
}
fun update() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
preferences.getStringSet(BARCODE_FORMATS, barcodeFormats)?.let {
barcodeFormats = addFormatsOnUpdate(
it,
BarcodeFormat.RMQR_CODE,
BarcodeFormat.DX_FILM_EDGE
)
fun profileLabel(context: Context, name: String?) = when (name) {
null -> context.getString(R.string.profile_default)
PROFILE_EXPERT -> context.getString(R.string.mode_expert)
PROFILE_SIMPLE -> context.getString(R.string.mode_simple)
else -> name
}
fun loadExpertProfile(context: Context) {
load(context, PROFILE_EXPERT)
}
fun loadSimpleProfile(context: Context) {
load(context, PROFILE_SIMPLE)
}
fun toJson(context: Context, name: String?): String {
val defaults = Preferences()
val p = if (name.isNullOrEmpty()) {
defaultPreferences
} else {
context.getSharedPreferences(name, Context.MODE_PRIVATE)
}
val json = JSONObject()
json.put("type", PROFILE_EXPORT_TYPE)
json.put("name", name ?: "")
json.put(BARCODE_FORMATS, JSONArray().apply {
(p.getStringSet(BARCODE_FORMATS, null) ?: defaults.barcodeFormats).forEach {
put(it)
}
})
json.put(SHOW_CROP_HANDLE, p, defaults.showCropHandle)
json.put(SHOW_CROSSHAIRS, p, defaults.showCrosshairs)
json.put(ZOOM_BY_SWIPING, p, defaults.zoomBySwiping)
json.put(AUTO_ROTATE, p, defaults.autoRotate)
json.put(TRY_HARDER, p, defaults.tryHarder)
json.put(BULK_MODE, p, defaults.bulkMode)
json.put(BULK_MODE_DELAY, p, defaults.bulkModeDelay)
json.put(SHOW_TOAST_IN_BULK_MODE, p, defaults.showToastInBulkMode)
json.put(VIBRATE, p, defaults.vibrate)
json.put(BEEP, p, defaults.beep)
json.put(BEEP_TONE_NAME, p, defaults.beepToneName)
json.put(BEEP_STREAM_NAME, p, defaults.beepStreamName)
json.put(USE_HISTORY, p, defaults.useHistory)
json.put(IGNORE_DUPLICATES_NAME, p, defaults.ignoreDuplicatesName)
json.put(IGNORE_CODES, p, ignoreCodesToJsonArray(defaults.ignoreCodes))
json.put(COPY_IMMEDIATELY, p, defaults.copyImmediately)
json.put(OPEN_IMMEDIATELY, p, defaults.openImmediately)
json.put(STRIP_TRACKING_PARAMS, p, defaults.stripTrackingParams)
json.put(SHOW_META_DATA, p, defaults.showMetaData)
json.put(SHOW_HEX_DUMP, p, defaults.showHexDump)
json.put(SHOW_CHECKSUM, p, defaults.showChecksum)
json.put(SHOW_RECREATION, p, defaults.showRecreation)
json.put(CLOSE_AUTOMATICALLY, p, defaults.closeAutomatically)
json.put(DEFAULT_SEARCH_URL, p, defaults.defaultSearchUrl)
json.put(OPEN_WITH_URL, p, defaults.openWithUrl)
json.put(SEND_SCAN_ACTIVE, p, defaults.sendScanActive)
json.put(SEND_SCAN_URL, p, defaults.sendScanUrl)
json.put(SEND_SCAN_TYPE, p, defaults.sendScanType)
json.put(SEND_SCAN_BLUETOOTH, p, defaults.sendScanBluetooth)
json.put(SEND_SCAN_BLUETOOTH_HOST, p, defaults.sendScanBluetoothHost)
json.put(CUSTOM_LOCALE, p, defaults.customLocale)
json.put(INDEX_OF_LAST_SELECTED_FORMAT, p, defaults.indexOfLastSelectedFormat)
json.put(INDEX_OF_LAST_SELECTED_EC_LEVEL, p, defaults.indexOfLastSelectedEcLevel)
json.put(FREE_ROTATION, p, defaults.freeRotation)
json.put(EXPAND_ESCAPE_SEQUENCES, p, defaults.expandEscapeSequences)
json.put(ADD_QUIET_ZONE, p, defaults.addQuietZone)
json.put(BRIGHTEN_SCREEN, p, defaults.brightenScreen)
json.put(PREVIEW_SCALE, p, defaults.previewScale)
json.put(AUTOMATED_ACTIONS, p.getString(AUTOMATED_ACTIONS, null) ?: "[]")
return json.toString()
}
fun importProfile(context: Context, text: String): String? {
val json = try {
JSONObject(text)
} catch (_: Exception) {
return null
}
if (json.optString("type") != PROFILE_EXPORT_TYPE) {
return null
}
var sanitized = json.optString("name").ifEmpty {
"imported"
}.replace(Regex("[^a-zA-Z0-9_-]+"), "_")
.trim('_')
if (sanitized.isEmpty()) {
sanitized = "imported"
}
val name = if (profiles.contains(sanitized)) {
val timestamp = SimpleDateFormat(
"yyyyMMdd_HHmmss",
Locale.US
).format(Date())
"${sanitized}_$timestamp"
} else {
sanitized
}
if (!addProfile(name)) {
return null
}
val target = context.getSharedPreferences(
name,
Context.MODE_PRIVATE
)
try {
target.edit {
val formatsArray = json.optJSONArray(BARCODE_FORMATS)
if (formatsArray != null) {
val formats = mutableSetOf<String>()
for (i in 0 until formatsArray.length()) {
val format = formatsArray.getString(i)
.migrateBarcodeFormatName()
try {
formats.add(BarcodeFormat.valueOf(format).name)
} catch (_: IllegalArgumentException) {
// Ignore unknown formats.
}
}
if (formats.isNotEmpty()) {
putStringSet(BARCODE_FORMATS, formats)
}
}
putBoolean(SHOW_CROP_HANDLE, json)
putBoolean(SHOW_CROSSHAIRS, json)
putBoolean(ZOOM_BY_SWIPING, json)
putBoolean(AUTO_ROTATE, json)
putBoolean(TRY_HARDER, json)
putBoolean(BULK_MODE, json)
putString(BULK_MODE_DELAY, json)
putBoolean(SHOW_TOAST_IN_BULK_MODE, json)
putBoolean(VIBRATE, json)
putBoolean(BEEP, json)
putString(BEEP_TONE_NAME, json)
putString(BEEP_STREAM_NAME, json)
putBoolean(USE_HISTORY, json)
putString(IGNORE_DUPLICATES_NAME, json)
putString(IGNORE_CODES, json)
putBoolean(COPY_IMMEDIATELY, json)
putBoolean(OPEN_IMMEDIATELY, json)
putBoolean(STRIP_TRACKING_PARAMS, json)
putBoolean(SHOW_META_DATA, json)
putBoolean(SHOW_HEX_DUMP, json)
putString(SHOW_CHECKSUM, json)
putBoolean(SHOW_RECREATION, json)
putBoolean(CLOSE_AUTOMATICALLY, json)
putString(DEFAULT_SEARCH_URL, json)
putString(OPEN_WITH_URL, json)
putBoolean(SEND_SCAN_ACTIVE, json)
putString(SEND_SCAN_URL, json)
putString(SEND_SCAN_TYPE, json)
putBoolean(SEND_SCAN_BLUETOOTH, json)
putString(SEND_SCAN_BLUETOOTH_HOST, json)
putString(CUSTOM_LOCALE, json)
putInt(INDEX_OF_LAST_SELECTED_FORMAT, json)
putInt(INDEX_OF_LAST_SELECTED_EC_LEVEL, json)
putBoolean(FREE_ROTATION, json)
putBoolean(EXPAND_ESCAPE_SEQUENCES, json)
putBoolean(ADD_QUIET_ZONE, json)
putBoolean(BRIGHTEN_SCREEN, json)
putFloat(PREVIEW_SCALE, json)
putString(AUTOMATED_ACTIONS, json)
}
} catch (_: Exception) {
removeProfile(name)
target.edit {
clear()
}
return null
}
return name
}
fun update() {
preferences.getStringSet(BARCODE_FORMATS, barcodeFormats)?.let {
barcodeFormats = addFormatsOnUpdate(
migrateCode39ToCode39Std(
migrateBarcodeFormatNames(it)
),
BarcodeFormat.Code39Std,
BarcodeFormat.Code39Ext,
BarcodeFormat.Code32,
BarcodeFormat.PZN,
BarcodeFormat.DataBarOmni,
BarcodeFormat.DataBarStk,
BarcodeFormat.DataBarLtd,
BarcodeFormat.DataBarExp,
BarcodeFormat.DataBarExpStk,
BarcodeFormat.RMQRCode,
BarcodeFormat.DXFilmEdge,
BarcodeFormat.Telepen,
BarcodeFormat.TelepenAlpha,
BarcodeFormat.TelepenNumeric
)
}
showCropHandle = preferences.getBoolean(
SHOW_CROP_HANDLE,
showCropHandle
)
showCrosshairs = preferences.getBoolean(
SHOW_CROSSHAIRS,
showCrosshairs
)
zoomBySwiping = preferences.getBoolean(ZOOM_BY_SWIPING, zoomBySwiping)
autoRotate = preferences.getBoolean(AUTO_ROTATE, autoRotate)
tryHarder = preferences.getBoolean(TRY_HARDER, tryHarder)
@@ -317,12 +552,17 @@ class Preferences {
preferences.getString(BEEP_TONE_NAME, beepToneName)?.also {
beepToneName = it
}
preferences.getString(BEEP_STREAM_NAME, beepStreamName)?.also {
beepStreamName = it
}
useHistory = preferences.getBoolean(USE_HISTORY, useHistory)
// Map old setting to new one if it wasn't on default value.
val ignoreConsecutiveDuplicates = "ignore_consecutive_duplicates"
if (!preferences.getBoolean(ignoreConsecutiveDuplicates, true)) {
preferences.edit().remove(ignoreConsecutiveDuplicates).apply()
preferences.edit {
remove(ignoreConsecutiveDuplicates)
}
ignoreDuplicatesName = "accept_duplicates"
}
@@ -332,6 +572,14 @@ class Preferences {
)?.also {
ignoreDuplicatesName = it
}
ignoreCodes = ignoreCodesFromJsonArray(
preferences.getString(
IGNORE_CODES,
ignoreCodesToJsonArray(
listOf(IgnoreCode(DEFAULT_IGNORE_CODE_PATTERN))
)
) ?: "[]"
)
copyImmediately = preferences.getBoolean(
COPY_IMMEDIATELY,
copyImmediately
@@ -340,6 +588,10 @@ class Preferences {
OPEN_IMMEDIATELY,
openImmediately
)
stripTrackingParams = preferences.getBoolean(
STRIP_TRACKING_PARAMS,
stripTrackingParams
)
showMetaData = preferences.getBoolean(SHOW_META_DATA, showMetaData)
showHexDump = preferences.getBoolean(SHOW_HEX_DUMP, showHexDump)
preferences.getString(SHOW_CHECKSUM, showChecksum)?.also {
@@ -401,8 +653,9 @@ class Preferences {
PREVIEW_SCALE,
previewScale
)
dynamicColors = preferences.getBoolean(DYNAMIC_COLORS, dynamicColors)
automatedActions = fromJsonArray(
preferences.getString(AUTOMATED_ACTIONS, "[]") ?: "[]"
preferences.getString(AUTOMATED_ACTIONS, null) ?: "[]"
)
}
@@ -411,19 +664,14 @@ class Preferences {
apply(AUTOMATED_ACTIONS, toJsonArray(actions))
}
private fun addFormatsOnUpdate(
restored: Set<String>,
vararg formats: BarcodeFormat
) = restored.toMutableSet().apply {
for (format in formats) {
val name = "${format.name}_added"
if (!preferences.getBoolean(name, false)) {
preferences.edit().putBoolean(name, true).apply()
add(format.name)
}
}
fun setIgnoreCodes(patterns: List<IgnoreCode>) {
ignoreCodes = patterns.toMutableList()
apply(IGNORE_CODES, ignoreCodesToJsonArray(patterns))
}
fun shouldIgnoreHistoryContent(content: String, format: String): Boolean =
ignoreCodes.any { it.matches(content, format) }
fun restoreCropHandle(
name: String,
viewWidth: Int,
@@ -446,39 +694,170 @@ class Preferences {
else -> ToneGenerator.TONE_PROP_BEEP
}
fun beepStream() = when (beepStreamName) {
"stream_notification" -> android.media.AudioManager.STREAM_NOTIFICATION
else -> android.media.AudioManager.STREAM_MUSIC
}
fun ignoreDuplicates() = when (ignoreDuplicatesName) {
"ignore_consecutive_duplicates" -> IgnoreDuplicates.Consecutive
"ignore_any_duplicates" -> IgnoreDuplicates.Any
else -> IgnoreDuplicates.Never
}
private fun put(label: String, value: Boolean) =
preferences.edit().putBoolean(label, value)
private fun migrateCode39ToCode39Std(
restored: Set<String>
) = restored.toMutableSet().apply {
// Migrate Code39 to the more specific Code39Std.
if (contains(BarcodeFormat.Code39.name)) {
remove(BarcodeFormat.Code39.name)
if (!contains(BarcodeFormat.Code39Std.name)) {
add(BarcodeFormat.Code39Std.name)
}
}
}
private fun migrateBarcodeFormatNames(formats: Set<String>): Set<String> {
return formats.mapTo(mutableSetOf()) { format ->
format.migrateBarcodeFormatName()
}
}
private fun addFormatsOnUpdate(
restored: Set<String>,
vararg formats: BarcodeFormat
) = restored.toMutableSet().apply {
for (format in formats) {
val name = "${format.name}_added"
if (!preferences.getBoolean(name, false)) {
preferences.edit {
putBoolean(name, true)
}
add(format.name)
}
}
}
private fun addPredefinedProfiles(context: Context) {
val name = "predefined_profiles_added"
if (defaultPreferences.getBoolean(name, false)) {
return
}
defaultPreferences.edit {
putBoolean(name, true)
}
profiles.add(PROFILE_EXPERT)
context.getSharedPreferences(PROFILE_EXPERT, Context.MODE_PRIVATE)
.edit {
clear()
applyExpertProfileSettings()
}
profiles.add(PROFILE_SIMPLE)
context.getSharedPreferences(PROFILE_SIMPLE, Context.MODE_PRIVATE)
.edit {
clear()
applySimpleProfileSettings()
}
saveProfiles()
}
private fun SharedPreferences.Editor.applyExpertProfileSettings() {
val defaults = Preferences()
putStringSet(BARCODE_FORMATS, defaults.barcodeFormats)
putBoolean(SHOW_CROP_HANDLE, defaults.showCropHandle)
putBoolean(SHOW_CROSSHAIRS, defaults.showCrosshairs)
putBoolean(ZOOM_BY_SWIPING, defaults.zoomBySwiping)
putBoolean(AUTO_ROTATE, defaults.autoRotate)
putBoolean(TRY_HARDER, defaults.tryHarder)
putBoolean(BULK_MODE, defaults.bulkMode)
putString(BULK_MODE_DELAY, defaults.bulkModeDelay)
putBoolean(SHOW_TOAST_IN_BULK_MODE, defaults.showToastInBulkMode)
putBoolean(VIBRATE, defaults.vibrate)
putBoolean(BEEP, defaults.beep)
putString(BEEP_TONE_NAME, defaults.beepToneName)
putString(BEEP_STREAM_NAME, defaults.beepStreamName)
putBoolean(USE_HISTORY, defaults.useHistory)
putString(IGNORE_DUPLICATES_NAME, defaults.ignoreDuplicatesName)
putString(IGNORE_CODES, ignoreCodesToJsonArray(defaults.ignoreCodes))
putBoolean(COPY_IMMEDIATELY, defaults.copyImmediately)
putBoolean(OPEN_IMMEDIATELY, defaults.openImmediately)
putBoolean(STRIP_TRACKING_PARAMS, defaults.stripTrackingParams)
putBoolean(SHOW_META_DATA, defaults.showMetaData)
putBoolean(SHOW_HEX_DUMP, defaults.showHexDump)
putString(SHOW_CHECKSUM, defaults.showChecksum)
putBoolean(SHOW_RECREATION, defaults.showRecreation)
putBoolean(CLOSE_AUTOMATICALLY, defaults.closeAutomatically)
putString(DEFAULT_SEARCH_URL, defaults.defaultSearchUrl)
putString(OPEN_WITH_URL, defaults.openWithUrl)
putBoolean(SEND_SCAN_ACTIVE, defaults.sendScanActive)
putString(SEND_SCAN_URL, defaults.sendScanUrl)
putString(SEND_SCAN_TYPE, defaults.sendScanType)
putBoolean(SEND_SCAN_BLUETOOTH, defaults.sendScanBluetooth)
putString(SEND_SCAN_BLUETOOTH_HOST, defaults.sendScanBluetoothHost)
putString(CUSTOM_LOCALE, defaults.customLocale)
putInt(INDEX_OF_LAST_SELECTED_FORMAT, defaults.indexOfLastSelectedFormat)
putInt(INDEX_OF_LAST_SELECTED_EC_LEVEL, defaults.indexOfLastSelectedEcLevel)
putBoolean(FREE_ROTATION, defaults.freeRotation)
putBoolean(EXPAND_ESCAPE_SEQUENCES, defaults.expandEscapeSequences)
putBoolean(ADD_QUIET_ZONE, defaults.addQuietZone)
putBoolean(BRIGHTEN_SCREEN, defaults.brightenScreen)
putFloat(PREVIEW_SCALE, defaults.previewScale)
putString(AUTOMATED_ACTIONS, toJsonArray(defaults.automatedActions))
}
private fun SharedPreferences.Editor.applySimpleProfileSettings() {
applyExpertProfileSettings()
putBoolean(SHOW_CROP_HANDLE, false)
putBoolean(ZOOM_BY_SWIPING, false)
putBoolean(BEEP, true)
putBoolean(COPY_IMMEDIATELY, true)
putBoolean(OPEN_IMMEDIATELY, true)
putBoolean(SHOW_META_DATA, false)
putBoolean(SHOW_HEX_DUMP, false)
putString(DEFAULT_SEARCH_URL, SIMPLE_SEARCH_URL)
putString(OPEN_WITH_URL, SIMPLE_SEARCH_URL)
putBoolean(EXPAND_ESCAPE_SEQUENCES, false)
putBoolean(BRIGHTEN_SCREEN, true)
}
private fun apply(label: String, value: Boolean) {
put(label, value).apply()
preferences.edit {
putBoolean(label, value)
}
}
private fun apply(label: String, value: String) {
preferences.edit().putString(label, value).apply()
preferences.edit {
putString(label, value)
}
}
private fun apply(label: String, value: Int) {
preferences.edit().putInt(label, value).apply()
preferences.edit {
putInt(label, value)
}
}
private fun apply(label: String, value: Float) {
preferences.edit().putFloat(label, value).apply()
preferences.edit {
putFloat(label, value)
}
}
@SuppressLint("ApplySharedPref")
private fun commit(label: String, value: String) {
preferences.edit().putString(label, value).commit()
preferences.edit(true) {
putString(label, value)
}
}
@RequiresApi(Build.VERSION_CODES.HONEYCOMB)
private fun apply(label: String, value: Set<String>) {
preferences.edit().putStringSet(label, value).apply()
preferences.edit {
putStringSet(label, value)
}
}
companion object {
@@ -486,10 +865,15 @@ class Preferences {
Consecutive, Any, Never
}
private const val PROFILE_EXPORT_TYPE = "binaryeye_profile"
private const val PROFILES = "profiles"
private const val PROFILE = "profile"
private const val PROFILE_EXPERT = "builtin:expert"
private const val PROFILE_SIMPLE = "builtin:simple"
private const val SIMPLE_SEARCH_URL = "https://www.google.com/search?q="
private const val BARCODE_FORMATS = "formats"
private const val SHOW_CROP_HANDLE = "show_crop_handle"
private const val SHOW_CROSSHAIRS = "show_crosshairs"
private const val ZOOM_BY_SWIPING = "zoom_by_swiping"
private const val AUTO_ROTATE = "auto_rotate"
private const val TRY_HARDER = "try_harder"
@@ -499,9 +883,12 @@ class Preferences {
private const val VIBRATE = "vibrate"
private const val BEEP = "beep"
private const val BEEP_TONE_NAME = "beep_tone_name"
private const val BEEP_STREAM_NAME = "beep_stream_name"
private const val USE_HISTORY = "use_history"
private const val IGNORE_DUPLICATES_NAME = "ignore_duplicates_name"
private const val IGNORE_CODES = "ignore_codes"
private const val OPEN_IMMEDIATELY = "open_immediately"
private const val STRIP_TRACKING_PARAMS = "strip_tracking_params"
private const val COPY_IMMEDIATELY = "copy_immediately"
private const val SHOW_META_DATA = "show_meta_data"
private const val SHOW_HEX_DUMP = "show_hex_dump"
@@ -524,5 +911,47 @@ class Preferences {
private const val BRIGHTEN_SCREEN = "brighten_screen"
private const val PREVIEW_SCALE = "preview_scale"
private const val AUTOMATED_ACTIONS = "automated_actions"
private const val DYNAMIC_COLORS = "dynamic_colors"
private const val DEFAULT_IGNORE_CODE_PATTERN = "^FIDO://.*"
}
}
private fun JSONObject.put(key: String, p: SharedPreferences, default: Boolean) {
put(key, p.getBoolean(key, default))
}
private fun JSONObject.put(key: String, p: SharedPreferences, default: String) {
put(key, p.getString(key, null) ?: default)
}
private fun JSONObject.put(key: String, p: SharedPreferences, default: Int) {
put(key, p.getInt(key, default))
}
private fun JSONObject.put(key: String, p: SharedPreferences, default: Float) {
put(key, p.getFloat(key, default))
}
private fun SharedPreferences.Editor.putBoolean(key: String, json: JSONObject) {
if (json.has(key)) {
putBoolean(key, json.getBoolean(key))
}
}
private fun SharedPreferences.Editor.putString(key: String, json: JSONObject) {
if (json.has(key)) {
putString(key, json.getString(key))
}
}
private fun SharedPreferences.Editor.putInt(key: String, json: JSONObject) {
if (json.has(key)) {
putInt(key, json.getInt(key))
}
}
private fun SharedPreferences.Editor.putFloat(key: String, json: JSONObject) {
if (json.has(key)) {
putFloat(key, json.getDouble(key).toFloat())
}
}
@@ -1,12 +1,12 @@
package de.markusfisch.android.binaryeye.preference
import android.content.Context
import android.support.v7.preference.DialogPreference
import android.util.AttributeSet
import androidx.preference.DialogPreference
import de.markusfisch.android.binaryeye.R
class UrlPreference(
context: Context?,
context: Context,
attrs: AttributeSet?
) : DialogPreference(context, attrs) {
private var url: String? = null
@@ -5,7 +5,7 @@ import android.app.PendingIntent
import android.content.Intent
import android.os.Build
import android.service.quicksettings.TileService
import android.support.annotation.RequiresApi
import androidx.annotation.RequiresApi
import de.markusfisch.android.binaryeye.activity.CameraActivity
@RequiresApi(Build.VERSION_CODES.N)
@@ -29,10 +29,14 @@ fun Context.errorFeedback() {
private fun Context.isSilent(): Boolean {
val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager
return when (am.ringerMode) {
AudioManager.RINGER_MODE_SILENT,
AudioManager.RINGER_MODE_VIBRATE -> true
return if (prefs.beepStream() == AudioManager.STREAM_MUSIC) {
am.getStreamVolume(AudioManager.STREAM_MUSIC) == 0
} else {
when (am.ringerMode) {
AudioManager.RINGER_MODE_SILENT,
AudioManager.RINGER_MODE_VIBRATE -> true
else -> false
else -> false
}
}
}
@@ -2,16 +2,18 @@ package de.markusfisch.android.binaryeye.view
import android.content.Context
import android.content.ContextWrapper
import android.graphics.drawable.ColorDrawable
import android.graphics.Color
import android.os.Build
import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.Toolbar
import android.view.View
import android.view.WindowManager
import android.widget.AbsListView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.drawable.toDrawable
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.color.MaterialColors
import de.markusfisch.android.binaryeye.R
val systemBarListViewScrollListener = object : AbsListView.OnScrollListener {
@@ -43,7 +45,7 @@ val systemBarRecyclerViewScrollListener = object : RecyclerView.OnScrollListener
layoutManager.findFirstCompletelyVisibleItemPosition() != 0
val scrollable = scrolled ||
layoutManager.findLastVisibleItemPosition() <
recyclerView.adapter.itemCount - 1
(recyclerView.adapter?.itemCount ?: 0) - 1
colorSystemAndToolBars(recyclerView.context, scrolled, scrollable)
}
@@ -62,24 +64,22 @@ private fun lastChildOutOfView(listView: AbsListView): Boolean {
}
fun AppCompatActivity.initBars() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Keeps the soft keyboard from repositioning the layout.
window.setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
)
window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
}
// Keeps the soft keyboard from repositioning the layout.
window.setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
)
window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
colorSystemAndToolBars(this)
setPaddingFromWindowInsets(
findViewById(R.id.main),
(findViewById(R.id.toolbar) as Toolbar).apply {
setSupportActionBar(this)
},
findViewById(R.id.navbar)?.apply {
setBackgroundColor(translucentPrimaryColor)
findViewById<View>(R.id.navbar)?.apply {
setBackgroundColor(getTranslucentPrimaryColor(this@initBars))
}
)
}
@@ -93,55 +93,49 @@ fun unlockStatusBarColor() {
statusBarColorLocked = false
}
private var translucentPrimaryColor = 0
private val actionBarBackground = ColorDrawable()
private fun getTranslucentPrimaryColor(context: Context): Int {
return ColorUtils.setAlphaComponent(
MaterialColors.getColor(
context,
com.google.android.material.R.attr.colorSurfaceContainerHigh,
Color.BLACK
),
0xd8
)
}
fun colorSystemAndToolBars(
context: Context,
scrolled: Boolean = false,
scrollable: Boolean = false
) {
if (translucentPrimaryColor == 0) {
translucentPrimaryColor = ContextCompat.getColor(
context,
R.color.primary_translucent
)
}
val translucentPrimaryColor = getTranslucentPrimaryColor(context)
val topColor = if (scrolled) translucentPrimaryColor else 0
val activity = getAppCompatActivity(context) ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// System bars no longer have a background from SDK35+.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
val window = activity.window
if (!statusBarColorLocked) {
@Suppress("DEPRECATION")
window.statusBarColor = topColor
}
// System bars no longer have a background from SDK35+.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
val window = activity.window
if (!statusBarColorLocked) {
@Suppress("DEPRECATION")
window.navigationBarColor = if (scrolled || scrollable) {
translucentPrimaryColor
} else {
0
}
window.statusBarColor = topColor
}
@Suppress("DEPRECATION")
window.navigationBarColor = if (scrolled || scrollable) {
translucentPrimaryColor
} else {
activity.findViewById(R.id.navbar)?.apply {
visibility = if (scrolled || scrollable) {
View.VISIBLE
} else {
View.GONE
}
0
}
} else {
activity.findViewById<View>(R.id.navbar)?.apply {
visibility = if (scrolled || scrollable) {
View.VISIBLE
} else {
View.GONE
}
setBackgroundColor(translucentPrimaryColor)
}
}
activity.supportActionBar?.setBackgroundDrawable(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
// Avoid allocation on Honeycomb and better.
actionBarBackground.color = topColor
actionBarBackground
} else {
// ColorDrawable.setColor() doesn't exist pre Honeycomb.
ColorDrawable(topColor)
}
)
activity.supportActionBar?.setBackgroundDrawable(topColor.toDrawable())
}
private fun getAppCompatActivity(context: Context): AppCompatActivity? {
@@ -2,12 +2,11 @@ package de.markusfisch.android.binaryeye.view
import android.graphics.Rect
import android.os.Build
import android.support.annotation.RequiresApi
import android.support.v4.view.ViewCompat
import android.support.v4.view.WindowInsetsCompat
import android.support.v7.widget.Toolbar
import android.view.View
import android.view.WindowInsets
import androidx.appcompat.widget.Toolbar
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
private var toolbarHeight = 0
fun setPaddingFromWindowInsets(
@@ -53,19 +52,15 @@ fun View.setPaddingFromWindowInsets() {
// For the original post see here:
// https://medium.com/androiddevelopers/windowinsets-listeners-to-layouts-8f9ccc8fa4d1
fun View.doOnApplyWindowInsets(f: (View, Rect) -> Unit) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
f(this, insetsWithToolbar())
} else {
ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->
f(v, insetsWithToolbar(insets))
insets
}
// It's important to explicitly request the insets (again) in
// case the view was created in Fragment.onCreateView() because
// setOnApplyWindowInsetsListener() won't fire when the view
// isn't attached.
requestApplyInsetsWhenAttached()
ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->
f(v, insetsWithToolbar(insets))
insets
}
// It's important to explicitly request the insets (again) in
// case the view was created in Fragment.onCreateView() because
// setOnApplyWindowInsetsListener() won't fire when the view
// isn't attached.
requestApplyInsetsWhenAttached()
}
private fun insetsWithToolbar(insets: WindowInsetsCompat? = null) = Rect(
@@ -75,7 +70,6 @@ private fun insetsWithToolbar(insets: WindowInsetsCompat? = null) = Rect(
insets?.systemWindowInsetBottom ?: 0
)
@RequiresApi(Build.VERSION_CODES.KITKAT)
private fun View.requestApplyInsetsWhenAttached() {
if (isAttachedToWindow) {
ViewCompat.requestApplyInsets(this)
@@ -3,20 +3,22 @@ package de.markusfisch.android.binaryeye.widget
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.graphics.Point
import android.graphics.Rect
import android.graphics.Region
import android.os.Build
import android.support.v4.content.ContextCompat
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.withSave
import com.google.android.material.color.MaterialColors
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.graphics.getBitmapFromDrawable
import de.markusfisch.android.binaryeye.graphics.getDashedBorderPaint
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
@@ -34,14 +36,27 @@ class DetectorView : View {
coordinatesLast = 0
invalidate()
}
private val roiPaint = context.getDashedBorderPaint()
private val overlayColor = ColorUtils.setAlphaComponent(
MaterialColors.getColor(
context,
com.google.android.material.R.attr.colorSecondaryContainer,
Color.WHITE
),
0x99
)
private val dotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = ContextCompat.getColor(context, R.color.dot)
color = overlayColor
style = Paint.Style.FILL
}
private val crosshairPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = overlayColor
style = Paint.Style.STROKE
strokeWidth = context.resources.displayMetrics.density
}
private val dotRadius: Float
private val handleBitmap = resources.getBitmapFromDrawable(
R.drawable.button_crop
R.drawable.button_crop,
context.theme
)
private val handleXRadius = handleBitmap.width / 2
private val handleYRadius = handleBitmap.height / 2
@@ -248,12 +263,21 @@ class DetectorView : View {
if (handleActive) {
canvas.drawClip()
}
if (prefs.showCrosshairs) {
canvas.drawCrosshairs()
}
canvas.drawDots()
if (prefs.showCropHandle) {
canvas.drawHandle()
}
}
fun getHorizontalCrosshairY() = if (handleActive) {
(roi.top + roi.bottom) * .5f
} else {
viewHeight * .5f
}
private fun Canvas.drawDots() {
var i = 0
while (i < coordinatesLast) {
@@ -270,10 +294,8 @@ class DetectorView : View {
if (minDist < 1) {
return
}
// canvas.clipRect() doesn't work reliably below KITKAT.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
val radius = min(minDist / 2, cornerRadius).toFloat()
save()
val radius = min(minDist / 2, cornerRadius).toFloat()
withSave {
clipOutPathCompat(
calculateRoundedRectPath(
roi.left.toFloat(),
@@ -285,9 +307,6 @@ class DetectorView : View {
)
)
drawColor(shadeColor)
restore()
} else {
drawRect(roi, roiPaint)
}
}
@@ -300,6 +319,28 @@ class DetectorView : View {
)
}
private fun Canvas.drawCrosshairs() {
val left: Float
val top: Float
val right: Float
val bottom: Float
if (handleActive) {
left = roi.left.toFloat()
top = roi.top.toFloat()
right = roi.right.toFloat()
bottom = roi.bottom.toFloat()
} else {
left = 0f
top = 0f
right = viewWidth.toFloat()
bottom = viewHeight.toFloat()
}
val cx = (left + right) * .5f
val cy = (top + bottom) * .5f
drawLine(cx, top, cx, bottom, crosshairPaint)
drawLine(left, cy, right, cy, crosshairPaint)
}
private fun updateClipRect() {
clampHandlePos()
val dx = abs(handlePos.x - center.x)
@@ -0,0 +1,40 @@
package de.markusfisch.android.binaryeye.zxingcpp
val oldToNewFormatNames = mapOf(
"NONE" to "None",
"AZTEC" to "Aztec",
"CODABAR" to "Codabar",
"CODE_39" to "Code39",
"CODE_39_EXT" to "Code39Ext",
"CODE_32" to "Code32",
"PZN" to "PZN",
"CODE_93" to "Code93",
"CODE_128" to "Code128",
"DATA_BAR" to "DataBar",
"DATA_BAR_OMNI" to "DataBarOmni",
"DATA_BAR_STK" to "DataBarStk",
"DATA_BAR_LTD" to "DataBarLtd",
"DATA_BAR_EXPANDED" to "DataBarExp",
"DATA_BAR_EXP_STK" to "DataBarExpStk",
"DATA_MATRIX" to "DataMatrix",
"DX_FILM_EDGE" to "DXFilmEdge",
"EAN_8" to "EAN8",
"EAN_13" to "EAN13",
"ITF" to "ITF",
"MAXICODE" to "MaxiCode",
"PDF_417" to "PDF417",
"TELEPEN" to "Telepen",
"TELEPEN_ALPHA" to "TelepenAlpha",
"TELEPEN_NUMERIC" to "TelepenNumeric",
"QR_CODE" to "QRCode",
"MICRO_QR_CODE" to "MicroQRCode",
"RMQR_CODE" to "RMQRCode",
"UPC_A" to "UPCA",
"UPC_E" to "UPCE"
)
/**
* Translate legacy barcode format names from ZXingC++ before 3.x
* to current BarcodeFormat enum names.
*/
fun String.migrateBarcodeFormatName() = oldToNewFormatNames[this] ?: this
+3
View File
@@ -0,0 +1,3 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorSecondaryContainer"/>
</selector>
@@ -0,0 +1,3 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorSecondaryContainer"/>
</selector>
@@ -0,0 +1,3 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorOnSecondaryContainer"/>
</selector>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

@@ -1,5 +1,5 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/primary"/>
<item android:drawable="?attr/colorSurface"/>
<item>
<bitmap
android:gravity="center"
@@ -1,8 +1,6 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/background_color"/>
<item>
<bitmap
android:gravity="top|right"
android:src="@drawable/wallpaper"/>
</item>
<item android:drawable="?attr/colorSurface"/>
<item
android:gravity="top|right"
android:drawable="@drawable/wallpaper"/>
</layer-list>
+24 -11
View File
@@ -1,12 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt" android:width="56dp" android:height="56dp" android:viewportWidth="56" android:viewportHeight="56">
<path android:pathData="M4 32a24 24 0 1 1 48 0 24 24 0 1 1-48 0">
<aapt:attr name="android:fillColor">
<gradient android:gradientRadius="24" android:centerX="28" android:centerY="32" android:type="radial">
<item android:offset="0" android:color="#FF000000"/>
<item android:offset="1" android:color="#00000000"/>
</gradient>
</aapt:attr>
</path>
<path android:pathData="M4 28a24 24 0 1 1 48 0 24 24 0 1 1-48 0" android:fillColor="#B6D46F"/>
<path android:pathData="M33 31h2v-8c0-1.1-0.9-2-2-2h-8v2h8v8zm-10 2V17h-2v4h-4v2h4v10c0 1.1 0.9 2 2 2h10v4h2v-4h4v-2H23z" android:fillColor="#FFFFFF"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="56dp"
android:height="56dp"
android:viewportWidth="56"
android:viewportHeight="56">
<path android:pathData="M4 32a24 24 0 1 1 48 0 24 24 0 1 1-48 0">
<aapt:attr name="android:fillColor">
<gradient
android:gradientRadius="24"
android:centerX="28"
android:centerY="32"
android:type="radial">
<item android:offset="0" android:color="#FF000000"/>
<item android:offset="1" android:color="#00000000"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M4 28a24 24 0 1 1 48 0 24 24 0 1 1-48 0"
android:fillColor="@color/crop_handle_fill"/>
<path
android:pathData="M33 31h2v-8c0-1.1-0.9-2-2-2h-8v2h8v8zm-10 2V17h-2v4h-4v2h4v10c0 1.1 0.9 2 2 2h10v4h2v-4h4v-2H23z"
android:fillColor="@color/crop_handle_icon"/>
</vector>
+2 -2
View File
@@ -4,13 +4,13 @@
<item android:state_pressed="false">
<shape android:shape="rectangle">
<corners android:radius="2dp"/>
<solid android:color="@color/accent"/>
<solid android:color="?attr/colorSecondary"/>
</shape>
</item>
<item android:state_pressed="true">
<shape android:shape="rectangle">
<corners android:radius="2dp"/>
<solid android:color="@color/accent_dark"/>
<solid android:color="?attr/colorSecondaryContainer"/>
</shape>
</item>
</selector>
+5 -11
View File
@@ -1,16 +1,10 @@
<selector
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:constantSize="true">
<item android:state_pressed="false">
android:color="?attr/colorControlHighlight">
<item>
<shape android:shape="rectangle">
<corners android:radius="2dp"/>
<solid android:color="@color/selected_row"/>
<solid android:color="?attr/colorSecondaryContainer"/>
</shape>
</item>
<item android:state_pressed="true">
<shape android:shape="rectangle">
<corners android:radius="2dp"/>
<solid android:color="@color/pressed_row"/>
</shape>
</item>
</selector>
</ripple>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M313 520l224 224-57 56-320-320 320-320 57 56-224 224h487v80H313z"/>
</vector>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M480 340q13 0 21.5-9t8.5-21q0-13-8.5-21.5T480 280q-12 0-21 8.5t-9 21.5q0 12 9 21t21 9zM172 595q-15-26-23.5-54.5T140 480q0-32 8.5-60.5T172 365l64 50q-8 15-12 31t-4 34q0 18 4 34t12 31l-64 50zM62 681q-29-44-45.5-94.5T0 480q0-56 16.5-106.5T62 279l63 50q-21 33-33 71t-12 80q0 42 12 80.5t33 71.5l-63 49zm298 119q-33 0-56.5-23.5T280 720V240q0-33 23.5-56.5T360 160h240q33 0 56.5 23.5T680 240v480q0 33-23.5 56.5T600 800H360zm0-80h240V240H360v480zm427-125l-64-50q8-15 12.5-31t4.5-34q0-18-4.5-34T723 415l64-50q15 26 24 54.5t9 60.5q0 32-9 60.5T787 595zm111 85l-63-49q21-33 33-71t12-80q0-42-12-80.5T835 328l63-49q29 43 45.5 93.5T960 479q0 56-16.5 106.5T898 680zm-538 40V240v480z"/>
</vector>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M440 880V576L256 760l-56-56 224-224-224-224 56-56 184 184V80h40l228 228-172 172 172 172-228 228h-40zm80-496l76-76-76-74v150zm0 342l76-74-76-76v150z"/>
</vector>
@@ -1,3 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<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="#fff" android:pathData="M20 8.69V4h-4.69L12 0.69 8.69 4H4v4.69L0.69 12 4 15.31V20h4.69L12 23.31 15.31 20H20v-4.69L23.31 12 20 8.69zM12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6 6 2.69 6 6-2.69 6-6 6zm0-10c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4z"/>
</vector>
@@ -1,3 +1,3 @@
<vector android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<vector android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M14 10H2v2h12v-2zm0-4H2v2h12V6zm4 8v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zM2 16h8v-2H2v2z"/>
</vector>
+1 -1
View File
@@ -1,3 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:tint="?attr/colorControlNormal" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
<path android:fillColor="#FFFFFFFF" android:pathData="M16 1H4C2.9 1 2 1.9 2 3v14h2V3h12V1zm3 4H8C6.9 5 6 5.9 6 7v14c0 1.1 0.9 2 2 2h11c1.1 0 2-0.9 2-2V7c0-1.1-0.9-2-2-2zm0 16H8V7h11v14z"/>
</vector>
@@ -1,3 +1,3 @@
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<vector android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M12.65 10C11.83 7.67 9.61 6 7 6c-3.31 0-6 2.69-6 6s2.69 6 6 6c2.61 0 4.83-1.67 5.65-4H17v4h4v-4h2v-4H12.65zM7 14c-1.1 0-2-0.9-2-2s0.9-2 2-2 2 0.9 2 2-0.9 2-2 2z"/>
</vector>
@@ -1,3 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:tint="?attr/colorControlNormal" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
<path android:fillColor="#FFFFFFFF" android:pathData="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M680 920V760H280q-33 0-56.5-23.5T200 680V280H40v-80h160V40h80v640h640v80H760v160h-80zm0-320V280H360v-80h320q33 0 56.5 23.5T760 280v320h-80z"/>
</vector>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M340.5 811.5Q275 783 226 734t-77.5-114.5Q120 554 120 480q0-44 10-85.5t29-78q19-36.5 45.5-68T264 192l272 272-56 56-216-216q-30 36-47 80.5T200 480q0 116 82 198t198 82q116 0 198-82t82-198q0-107-68.5-184.5T520 204v76h-80V120h40q74 0 139.5 28.5T734 226q49 49 77.5 114.5T840 480q0 74-28.5 139.5T734 734q-49 49-114.5 77.5T480 840q-74 0-139.5-28.5zm-89-303Q240 497 240 480t11.5-28.5Q263 440 280 440t28.5 11.5Q320 463 320 480t-11.5 28.5Q297 520 280 520t-28.5-11.5zm200 200Q440 697 440 680t11.5-28.5Q463 640 480 640t28.5 11.5Q520 663 520 680t-11.5 28.5Q497 720 480 720t-28.5-11.5zm200-200Q640 497 640 480t11.5-28.5Q663 440 680 440t28.5 11.5Q720 463 720 480t-11.5 28.5Q697 520 680 520t-28.5-11.5z"/>
</vector>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M440 160v487L216 423l-56 57 320 320 320-320-56-57-224 224V160h-80z"/>
</vector>
+1 -1
View File
@@ -1,3 +1,3 @@
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<vector android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c0.39-0.39 0.39-1.02 0-1.41l-2.34-2.34c-0.39-0.39-1.02-0.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/>
</vector>
@@ -1,3 +1,3 @@
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<vector android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M9 3L5 6.99h3V14h2V6.99h3L9 3zm7 14.01V10h-2v7.01h-3L15 21l4-3.99h-3z"/>
</vector>
@@ -1,3 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:tint="?attr/colorControlNormal" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
<path android:fillColor="#FFFFFFFF" android:pathData="M7 2v11h3v9l7-12h-4l4-8z"/>
</vector>

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