Compare commits

...
202 Commits
Author SHA1 Message Date
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
Markus Fisch 1fab30089e Advance version number to 1.68.2 2026-02-22 21:15:25 +01:00
Markus Fisch f9bf16dd47 Update ZXingCpp 2026-02-22 21:03:20 +01:00
Markus Fisch 7a59126312 Use 1 as the new default for margin/quiet zone
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-22 20:58:24 +01:00
Markus Fisch 694c854650 Advance version number to 1.68.1 2026-02-21 21:50:51 +01:00
Markus Fisch b1cd12ec77 Validate frame data before giving it to ZXing
And refactor preview callback to use an early exit for clarity.

According to Vitals, `frameData` can apparently be `null` on
some devices sometimes.
2026-02-21 21:40:38 +01:00
Markus Fisch 0c27b15025 Check if fragment is still added
Before setting brightness, which may happen later because
it's called from the queue.
2026-02-21 21:20:31 +01:00
Markus Fisch 5de950bdbf Update tools version 2026-02-21 19:14:54 +01:00
Markus Fisch ae9e304ef6 Handle bad decoded intent parcelables safely
Guard reading the `decoded` Scan extra in MainActivity against
framework unparcel exceptions. Some external or malformed intents can
trigger a NullPointerException in Parcel before app-level checks run.

Return null when unparceling fails so the app falls back to the default
DecodeFragment path instead of crashing on launch.
2026-02-21 19:13:47 +01:00
Dominik StadlerandMarkus Fisch c4cc14554b Clear search-term on empty input, not on "Cancel"
Clearing search-term on cancel and not on empty input is
unexpected. "Cancel" usually does not change the state.

Rather clear the searchTerm on empty input and do not
handle the "Cancel"-button at all.
2026-02-20 13:52:50 +01:00
Dominik StadlerandMarkus Fisch d6afccf562 Apply some IDE suggestions 2026-02-20 13:52:50 +01:00
Markus Fisch a90bdad117 Update ZXingCpp
And remove the now missing `returnCodabarStartEnd` feature.
2026-02-13 19:05:35 +01:00
solokotandGitHub 52d4f082f2 Update Russian translation
Small Improvements
2026-02-12 19:45:38 +01:00
Markus Fisch 1c63f5b3ac Advance version number to 1.68.0 2026-02-07 20:18:43 +01:00
Markus Fisch 21985b4533 Remove deprecated flags from gradle.properties
Stuff only added because Google deemed it necessary.
Now the Google propeller caps have apparently changed
their minds. Don't care.
2026-02-07 20:15:41 +01:00
bovirusandGitHub 012ac644aa Italian language update 2026-02-07 12:21:26 +01:00
Markus Fisch 19f19739fd Fix up navigation for setting fragments 2026-02-06 17:42:18 +01:00
Markus Fisch f01f9fcd91 Add a dedicated fragement for managing profiles
And the missing option to remove profiles.
2026-02-06 17:42:18 +01:00
Markus Fisch 4177b248c3 Clean up AutomatedActionsFragment 2026-02-06 17:42:18 +01:00
Markus Fisch e4182a265b Add a ViewHolder to ActionsAdapter 2026-02-06 17:42:18 +01:00
Markus Fisch 98df5a447c Move adding an action into its own function
For readability.
2026-02-06 17:42:18 +01:00
Markus Fisch a0a1405359 Allow alphanumeric profile names only
Since a profile name is also used as a preference name,
it cannot contain path separators or anything like that.
So keep names alphanumeric for robustness.
2026-02-06 17:42:18 +01:00
bovirusandGitHub 76922c421c Italian language update 2026-02-06 17:41:42 +01:00
Markus Fisch cac9dd16da Add automated actions
To automatically execute actions when certain barcodes
are scanned.
2026-02-04 22:40:39 +01:00
Markus Fisch a1f9ba1056 Update gradle wrapper 2026-02-04 21:45:58 +01:00
Markus Fisch 0b67c272b6 Update robolectric to latest version 2026-02-03 19:16:54 +01:00
Markus Fisch 9b8be5305d Update tools and gradle wrapper to Gradle 9
And remove the now unnecessary kotlin-android plugin.
2026-02-03 18:15:33 +01:00
Markus Fisch a026ade13b Remove own toHexString() implementation
In favor of the new Kotlin API.
2026-01-26 20:31:07 +01:00
Markus Fisch b1771bc5ea Advance version number to 1.67.1 2026-01-14 23:19:07 +01:00
Markus Fisch 5199460669 Open file:// Uris as documents from Android N+
Because file:// can't be used with Scoped Storage anymore.
2026-01-14 23:16:05 +01:00
Markus Fisch fd040e1660 Remove lateinit from PickActivity members
Because `onCreate()` may return before the `lateinit`
members have been initialized, which would raise a
`kotlin.UninitializedPropertyAccessException`.
2026-01-13 14:53:27 +01:00
Markus Fisch 2e1528364e Fix auto closing of DecodeFragment
Some actions require the `Activity` object to stay alive, e.g.
`OpenOrSearchAction` or `WifiAction` may show a dialog first and
only then may trigger `startActivity`.

Before this commit, with the preferences `openImmediately` or
`closeAutomatically` enabled, the app crashed for content that
would show a dialog through `DecodeFragment.executeAction()`.
2026-01-13 21:20:35 +01:00
KamaleiZestriandGitHub d21af142bb Catch all exceptions for socket.connect()
`socket.connect()` can throw `IOException`s, too.

Occurs with Windows when the bluetooth service gets killed and
restarted in the background.
2026-01-13 09:52:50 +01:00
Markus Fisch 8395e135f9 Update Kotlin version 2025-12-11 20:04:25 +01:00
Markus Fisch c0caa0ce44 Update tools version 2025-12-11 20:03:10 +01:00
Markus Fisch 96927ef9b2 Add line breaks in README 2025-11-14 19:11:18 +01:00
TheRedCyclopsandGitHub 84d9a3b55a Remove double "at" in README 2025-11-14 11:14:34 +01:00
Markus Fisch ffe8e4c5ba Advance version number to 1.67.0 2025-10-10 20:06:03 +02:00
Markus Fisch bc7c5dca97 Fix restoring crop and zoom on profile changes 2025-10-09 20:45:52 +02:00
Markus Fisch d9737f4df1 Store crop handle position per view size
So we have a crop handle configuration for each view size.
2025-10-05 22:42:00 +02:00
Markus Fisch 0e8147713a Use default preferences for welcome screen marker
Since the welcome screen is intended to be shown for the first
start only.
2025-10-05 22:39:54 +02:00
Markus Fisch 3d425a097a Update robolectric version 2025-09-30 23:24:26 +02:00
Markus Fisch c26765cc63 Update Kotlin version 2025-09-30 23:22:15 +02:00
Markus Fisch fb6dc8ba55 Update kotlinx coroutines library 2025-09-30 23:21:54 +02:00
Markus Fisch 70f4d166a0 Support having multiple preference profiles
Requested in #558.
2025-09-30 23:20:30 +02:00
Markus Fisch 6c136ce719 Support regex when searching for a barcode 2025-09-30 16:36:14 +02:00
Markus Fisch c6dee80c0a Quote search term and remove comma
Between search term and barcode restriction.

Cleaner and less irritating.
2025-09-29 22:24:54 +02:00
Markus Fisch 1f43dd4827 Refactor scan_format to scan_for
Since this label is used for barcode formats and search terms.
2025-09-29 22:19:56 +02:00
Markus Fisch a366157977 Fix setting title with format and search term 2025-09-29 22:18:30 +02:00
Markus Fisch 6bb72672a3 Show a message for unmatching barcodes
So it's clear that the app recognized the barcode, but it's
not the one we're looking for.
2025-09-29 22:12:26 +02:00
Markus Fisch 04774527a0 Fix string capitalization for consistency 2025-09-29 21:36:44 +02:00
Markus Fisch 6f3e644aa8 Update tools version 2025-09-23 21:48:02 +02:00
Markus Fisch 5e3176b703 Update ZXingC++ for 16 KB ELF alignment
Because starting November 1st, 2025, all new apps and updates to
existing apps submitted to Google Play and targeting Android 15+
devices must support 16 KB page sizes on 64-bit devices.
2025-09-23 21:38:46 +02:00
zicaandGitHub 0f0aaff284 Improve zh-tw and ja translations 2025-09-12 18:39:29 +02:00
Markus Fisch f0d09e81b4 Advance version number to 1.66.2 2025-08-26 21:16:08 +02:00
Markus Fisch 771258cb5c Update tools version 2025-08-18 20:33:59 +02:00
Markus Fisch 9482086666 Reformat code 2025-08-18 20:33:21 +02:00
CertainBotandGitHub bcad50c3e8 Update Spanish translation
Few corrections over the AI translations.
2025-08-08 09:45:07 +02:00
Markus Fisch dbd055699e Remove adding ?content= to forward URL
When no other query was given.

This was meant to make it clear that the scanned content will
just be added to the given URL. But it's too restrictive as
there can be URLs without query strings.
2025-08-04 21:52:05 +02:00
Markus Fisch d67f8273d6 Update to target SDK 36 2025-07-30 21:03:48 +02:00
Markus Fisch 649be0ceb6 Update Kotlin version 2025-07-30 20:59:22 +02:00
Markus Fisch f5eb5dd8d2 Update gradle wrapper 2025-07-30 20:52:46 +02:00
Markus Fisch eb805dad2f Update tools version and gradle wrapper 2025-06-11 17:44:40 +02:00
Markus Fisch 202bb32b29 Advance version number to 1.66.1 2025-05-28 18:33:03 +02:00
Markus Fisch 50b8a4d329 Add missing curly braces
We always use curly braces around here.

When in doubt, google for "Apple's SSL Bug".
2025-05-28 18:27:26 +02:00
Markus Fisch fa15478fb4 Remove unused parameter from Bluetooth send method 2025-05-28 18:26:51 +02:00
Markus Fisch 20c2a5a8aa Catch Bluetooth SecurityExceptions explicitly
These are thrown when the user denies access to Bluetooth,
and `$ ./gradlew lintDebug` complains rightly about this.
2025-05-28 18:25:10 +02:00
Markus Fisch 00b95678ed Remove now unnecessary cast 2025-05-19 21:31:39 +02:00
Markus Fisch 5df3517563 Use an underscore for unused exception parameters
To get rid of the warnings.
2025-05-19 21:29:02 +02:00
Markus Fisch fd6946d2c2 Fix checking scan.id instead of fragment id 2025-05-19 21:05:22 +02:00
Markus Fisch c74d229e1f Update tools and gradle wrapper 2025-05-19 21:04:33 +02:00
Unknownman820andGitHub fa99a19db5 Update chinese translation 2025-05-07 19:07:44 +02:00
Markus Fisch b083fcf213 Show elements of invalid Wifi configurations too
So users can still access the elements even when the configuration
is invalid.
2025-05-07 18:47:34 +02:00
Markus Fisch 46930ddea6 Unescape Wifi elements in parseMap()
Instead of unescaping elements selectively in `SimpleDataAccessor`.

This is much simpler and this way the `Map` `parseMap()` returns has
always unescaped values. This `Map` is also layed out in the
`DecodeFragment`, which still displayed the escaped values before
this commit.
2025-05-07 18:47:34 +02:00
Markus Fisch 7bef77b96e Add test for unescaping emojis in Wifi credentials
Because of #544.
2025-05-07 18:47:34 +02:00
NathanandGitHub c5f4c088b4 Update french locale
Correction of automatic translations.
2025-05-01 21:58:36 +02:00
Yurt PageandGitHub 73f7210e18 Update translations from Toolate 2025-04-25 23:50:20 +02:00
Markus Fisch 469429e663 Advance version number to 1.66.0 2025-04-25 23:49:16 +02:00
Markus Fisch 1a7ac59740 Refactor BinaryEyeDeeplink to just Deeplink 2025-04-25 12:54:50 +02:00
Markus Fisch c13c59e728 Refactor IDLParser/ElementMap to use camel case
And match these names with other symbols in this code base
that don't use all caps for abbreviations like `Url` or
`UrlDialogFragment` and so on.
2025-04-25 12:51:30 +02:00
Markus Fisch bc17e25717 Reformat with Android Studio 2025-04-25 12:48:51 +02:00
Yurt PageandGitHub 5017bd4c8d fastlane: simplify short description
And add "QR code" to make it easier to find.
2025-04-25 12:43:58 +02:00
Anton SerebryakovandGitHub a44a913eca Support deep links to encode content via web links
To open barcode://encode via a HTTP deep link.

Also adds actions to copy the deep link to the clipboard.
2025-04-25 12:42:42 +02:00
Yurt PageandGitHub 007e83e243 fastlane: merge summary.txt into short_description.txt
summary.txt was used by Tripple-T but the default Fastlane file name is short_description.txt. It supports the summary.txt too but we have a duplication problem and some languages has both.
So I removed the duplicated summary.txt and renamed existing summary.txt into short_description.txt.
2025-04-23 21:43:47 +02:00
Markus Fisch de8f97c246 Parse International Driver Licenses
Usually from PDF417 barcodes.
2025-04-06 15:10:36 +02:00
Markus Fisch 1287591e53 Clean up Bluetooth instructions
Use proper bold syntax instead of making things headlines that
aren't headlines. Markdown is not source code for HTML but a human
readable document format. So semantics are important.

Also removes detailed install instructions for the companion apps.
These should be in the README of the corresponding application.
2025-04-03 21:44:40 +02:00
Markus Fisch 8c17238572 Only show clipboard toast before Android 13
Because there is a clipboard popup from Android 13 on.
2025-04-03 09:17:12 +02:00
Curious, aren't we?andGitHub 5dbb36e6a9 Add Bluetooth howto to the README 2025-04-02 23:33:14 +02:00
Markus Fisch ca7dfb3a33 Add new WiFi's via Settings intent
From API level 30, Android R, on.

This has the advantage that Binary Eye doesn't take ownership of
the newly added WiFi. See #482
2025-03-30 19:53:12 +02:00
Markus Fisch 419b7d5765 Give share file its own icon
So that we can clearly distinguish this action from the share
action when all actions are layed out next to each other.
2025-03-16 21:20:23 +01:00
Markus Fisch fa38866669 Update missing string resources
With a little help from the OpenAI API ;)
2025-03-27 14:19:44 +01:00
bovirusandGitHub 19ca521f59 Italian language update 2025-03-16 11:43:37 +01:00
Markus Fisch be657d86cf Show parsed data for MatMsgAction and WebAction 2025-03-14 12:27:06 +01:00
Markus Fisch 5199a4a526 Remove log messages
From debugging.
2025-03-14 12:27:06 +01:00
Markus Fisch ff48a543c9 Show parsed VType items in data view 2025-03-14 12:27:06 +01:00
Markus Fisch 2eadd01a87 Use apply scope function initialize valueView
Removes repetitions.
2025-03-14 12:27:06 +01:00
Markus Fisch dd2abe7d89 Copy password as every other content
And remove the corresponding special message.

It's probably clear enough with the generic message.
2025-03-14 12:27:06 +01:00
Markus Fisch 8e05caac70 Add click listener to copy data items
Into the clipboard for easy access.

Now, the data items should be bigger and also have some click feedback.
2025-03-14 12:27:06 +01:00
Markus Fisch 46aeced09f Separate Deep Links from Intents in README
And add some links on how to integrate a barcode scanner in an app.
2025-03-14 12:27:06 +01:00
NathanandGitHub 3681c5d011 Update french translation
Added strings for latest features.
2025-03-14 12:23:53 +01:00
Markus Fisch 20b4f7aac5 Advance version number to 1.65.2 2025-03-12 18:55:50 +01:00
Markus Fisch bc6e350271 Sanity check view, ROI and frame metrics
Because `roundToInt` in `Rect.setFrameRoi()` throws an
`IllegalArgumentException: Cannot round NaN value` for
some people out there.
2025-03-12 18:52:27 +01:00
Markus Fisch c9c1b2f5bf Advance version number to 1.65.1 2025-03-12 12:23:43 +01:00
Markus Fisch 1dbc0f0fa1 Fix fastlane changelogs
And insert the missing log for version code 141.
2025-03-12 12:20:20 +01:00
solokotandGitHub ab8170ee72 Update Russian translation
Fix machine translation
2025-03-12 12:11:05 +01:00
Markus Fisch 551464dc6f Refactor for an early return when content is empty
Easier to read and less indent.
2025-03-12 00:54:59 +01:00
Markus Fisch c3aac0a1e1 Set a default search URL for simple mode
So any (otherwise unregistered) content is always redirected to
the browser.
2025-03-12 00:53:37 +01:00
Markus Fisch d678e1cfd0 Hide keyboard when expanding recreated barcode
So the layout of `BarcodeFragment` isn't affected by it.
2025-03-12 00:49:30 +01:00
Markus Fisch a445ade877 Reduce height for 1D barcodes
1D barcodes don't need as much vertical space.
2025-03-12 00:48:20 +01:00
Markus Fisch 1fd0b7637f Add execute argument for binaryeye://encode
For immediate code generation.
2025-03-12 00:37:47 +01:00
Markus Fisch fdc88df897 Remember quiet zone settings
And use a quiet zone by default because it's recommended.
2025-03-12 00:19:41 +01:00
Markus Fisch d0f133ae00 Fix showing a scan from history
When `openImmediately` is `true`.

Of course, the action should only be executed when we're coming
from the scanner, not the history.
2025-03-12 00:11:14 +01:00
Markus Fisch 67646261e7 Remove unnecessary id field 2025-03-12 00:08:11 +01:00
240 changed files with 12054 additions and 3759 deletions
+104
View File
@@ -1,5 +1,109 @@
# Change Log
## 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
## 1.68.1
* Improve UI of search barcode dialog
* Stability improvements
* Update ZXingCpp
* Update Russian translation
## 1.68.0
* Add automated actions
* Improve managing profiles
## 1.67.1
* Fix opening file:// Uris
* Fix auto-return/immediate opening
* Improve Bluetooth stability
## 1.67.0
* Add preference profiles
* Support regex when searching for a barcode
* Improve feedback when searching for a barcode
* Update translations
## 1.66.2
* Remove forcing `?content=` on forwarding URL
* Update Spanish translation
## 1.66.1
* Show elements of invalid Wifi configurations too
* Update translations
## 1.66.0
* Support deep links to encode content via web links
* Parse International Driver Licenses (IDL) in PDF417 barcodes
* Add WiFis from system settings
* Show parsed data items
* Update translations
## 1.65.2
* Sanity check ROI metrics
## 1.65.1
* Improve simple mode
* Remember quiet zone settings
* Reduce height for 1D barcodes
* Add execute argument for binaryeye://encode
* Update Russian translation
## 1.65.0
* Add a welcome screen to choose a mode
* Add a deep link to encode a barcode
+16 -1
View File
@@ -31,7 +31,22 @@ testscan:
testencode:
adb shell am start -a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d 'binaryeye://encode?content=Test\&format=QRCode'
-d 'binaryeye://encode?content=Test\&format=QR_CODE'
testencodenotqr:
adb shell am start -a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d 'binaryeye://encode?content=Test\&format=DATA_MATRIX'
testencodeexec:
adb shell am start -a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d 'binaryeye://encode?content=Test\&format=QR_CODE\&execute'
testencodeurl:
adb shell am start -a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d 'http://markusfisch.de/encode?content=Test\&format=QR_CODE'
testurl:
adb shell am start -a android.intent.action.VIEW \
+37 -8
View File
@@ -83,14 +83,12 @@ ZXing can generate the following barcode formats:
* [QR CODE][qr_code]
* [UPC A][upc_a]
## Deep Links and Intents
### Deep Links
## Deep Links
You can invoke Binary Eye with a web URI intent from anything that can
open URIs.
#### Decoding
### Decoding
1. [binaryeye://scan](binaryeye://scan) (note: GitHub does not render this as clickable link)
2. [http(s)://markusfisch.de/BinaryEye](http://markusfisch.de/BinaryEye)
@@ -106,19 +104,32 @@ Supported symbols are:
* `RESULT_BYTES` - raw result as a hex string
* `FORMAT` - barcode format
#### Encoding
### Encoding
1. [binaryeye://encode](binaryeye://encode)
You can use the URL arguments `content` and `format` to automatically
preset this data. For example:
[binaryeye://encode?content=Test&format=QRCODE](binaryeye://encode?content=Test&format=QRCODE)
1. [binaryeye://encode?content=Test&format=QR_CODE](binaryeye://encode?content=Test&format=QR_CODE)
2. [http(s)://markusfisch.de/encode?content=Test&format=QR_CODE](http://markusfisch.de/encode?content=Test&format=QR_CODE)
3. [http(s)://markusfisch.de/encode?content=Test&format=DATA_MATRIX](http://markusfisch.de/encode?content=Test2&format=DATA_MATRIX&execute)
If you want the code to be generated immediately, add `execute=true` (or
just `execute`).
## Intents
You can also use Binary Eye from other apps by using an [Intent][intent].
If you prefer to integrate a barcode scanner into your app, take a look at
[BarcodeScannerView][barcode_scanner_view] (if you also want to use
[ZXing-C++][zxing_cpp]) or read how to
[scan barcodes with ML Kit on Android][mlkit_barcode_scanning].
### SCAN Intent
You can also use Binary Eye from other apps by using the
`com.google.zxing.client.android.SCAN` Intent with
Use the `com.google.zxing.client.android.SCAN` `Intent` with
[startActivityForResult()][start_activity] like this:
```kotlin
@@ -166,6 +177,21 @@ class YourActivity : AppCompatActivity() {
}
```
## Forwarding scans via Bluetooth
**Note:** The companion apps aren't developed nor maintained by the
author of Binary Eye.
In order to set up Bluetooth forwarding to a computer:
1. Pair your devices (phone and pc) if not paired already
2. Download and run a companion app:
- Windows: https://github.com/KamaleiZestri/BinaryReceptorWindows
- Linux: https://github.com/sean666888/bin_eye_bt_receiver
3. In the settings of Binary Eye, enable "Forward scans with Bluetooth"
and select the appropriate target host device that is running the
companion application.
[play]: https://play.google.com/store/search?q=barcode%20scanner&c=apps
[zxing_cpp]: https://github.com/zxing-cpp/zxing-cpp
[kotlin]: http://kotlinlang.org/
@@ -188,6 +214,9 @@ class YourActivity : AppCompatActivity() {
[upc_a]: https://en.wikipedia.org/wiki/Universal_Product_Code
[upc_e]: https://en.wikipedia.org/wiki/Universal_Product_Code#UPC-E
[upc_ean]: https://en.wikipedia.org/wiki/Universal_Product_Code#EAN-13
[intent]: https://developer.android.com/reference/android/content/Intent
[barcode_scanner_view]: https://github.com/markusfisch/BarcodeScannerView
[mlkit_barcode_scanning]: https://developers.google.com/ml-kit/vision/barcode-scanning/android
[start_activity]: https://developer.android.com/reference/android/app/Activity#startActivityForResult(android.content.Intent,%20int)
[on_activity_result]: https://developer.android.com/reference/android/app/Activity#onActivityResult(int,%20int,%20android.content.Intent)
[intent_result]: https://developer.android.com/training/basics/intents/result
+22 -16
View File
@@ -1,16 +1,15 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
namespace "de.markusfisch.android.binaryeye"
compileSdk sdk_version
defaultConfig {
minSdk 9
minSdk 21
targetSdk sdk_version
versionCode 145
versionName '1.65.0'
versionCode 166
versionName '1.72.0'
}
signingConfigs {
@@ -35,7 +34,7 @@ android {
}
release {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
minifyEnabled true
shrinkResources true
signingConfig signingConfigs.release
@@ -63,27 +62,34 @@ android {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.7.1'
implementation "androidx.core:core-ktx:1.17.0"
// 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"
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:v2.3.0.3'
implementation 'androidx.camera:camera-core:1.4.2'
implementation 'androidx.camera:camera-camera2:1.4.2'
implementation 'androidx.camera:camera-lifecycle:1.4.2'
implementation 'androidx.camera:camera-view:1.4.2'
implementation 'com.github.markusfisch:ScalingImageView:1.4.3'
implementation 'com.github.markusfisch:zxing-cpp:v3.0.2.3'
implementation 'de.tsenger:vdstools:0.16.0-SNAPSHOT'
implementation 'org.bouncycastle:bcpkix-jdk15to18:1.83'
// 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.** { *; }
+36 -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,8 +112,23 @@
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"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:host="markusfisch.de"/>
<data android:pathPrefix="/encode"/>
<data android:scheme="http"/>
<data android:scheme="https"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
@@ -121,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"
@@ -133,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.
@@ -6,15 +6,17 @@ import de.markusfisch.android.binaryeye.content.execShareIntent
import de.markusfisch.android.binaryeye.content.openUrl
import de.markusfisch.android.binaryeye.widget.toast
interface IAction {
val iconResId: Int
val titleResId: Int
abstract class Action {
abstract val iconResId: Int
abstract val titleResId: Int
fun canExecuteOn(data: ByteArray): Boolean
suspend fun execute(context: Context, data: ByteArray)
var fired: Boolean = false
abstract fun canExecuteOn(data: ByteArray): Boolean
abstract suspend fun execute(context: Context, data: ByteArray)
}
abstract class IntentAction : IAction {
abstract class IntentAction : Action() {
abstract val errorMsg: Int
final override suspend fun execute(context: Context, data: ByteArray) {
@@ -22,7 +24,7 @@ abstract class IntentAction : IAction {
if (intent == null) {
context.toast(errorMsg)
} else {
context.execShareIntent(intent)
fired = context.execShareIntent(intent)
}
}
@@ -32,7 +34,7 @@ abstract class IntentAction : IAction {
): Intent?
}
abstract class SchemeAction : IAction {
abstract class SchemeAction : Action() {
abstract val scheme: String
open val buildRegex: Boolean = false
@@ -50,6 +52,6 @@ abstract class SchemeAction : IAction {
}
final override suspend fun execute(context: Context, data: ByteArray) {
context.openUrl(String(data))
fired = context.openUrl(String(data))
}
}
@@ -12,9 +12,9 @@ import de.markusfisch.android.binaryeye.actions.web.WebAction
import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
object ActionRegistry {
val DEFAULT_ACTION: IAction = OpenOrSearchAction
val DEFAULT_ACTION: Action = OpenOrSearchAction
private val REGISTRY: Set<IAction> = setOf(
private val REGISTRY: Set<Action> = setOf(
MailAction,
MatMsgAction,
OtpauthAction,
@@ -28,7 +28,7 @@ object ActionRegistry {
WebAction
)
fun getAction(data: ByteArray): IAction = REGISTRY.find {
fun getAction(data: ByteArray): Action = REGISTRY.find {
it.canExecuteOn(data)
} ?: DEFAULT_ACTION
}
@@ -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))
@@ -2,62 +2,65 @@ package de.markusfisch.android.binaryeye.actions.search
import android.content.Context
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IAction
import de.markusfisch.android.binaryeye.actions.Action
import de.markusfisch.android.binaryeye.app.alertDialog
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.content.openUrl
import de.markusfisch.android.binaryeye.net.urlEncode
object OpenOrSearchAction : IAction {
object OpenOrSearchAction : Action() {
override val iconResId: Int = R.drawable.ic_action_search
override val titleResId: Int = R.string.search_web
override fun canExecuteOn(data: ByteArray): Boolean = false
override suspend fun execute(context: Context, data: ByteArray) {
view(context, String(data), true)
}
private suspend fun view(
context: Context,
url: String,
search: Boolean
) {
if (!context.openUrl(url, silent = true) && search) {
openSearch(context, url)
}
}
private suspend fun openSearch(context: Context, query: String) {
val defaultSearchUrl = prefs.defaultSearchUrl
if (defaultSearchUrl.isNotEmpty()) {
view(
context,
defaultSearchUrl + query.urlEncode(),
false
)
return
}
val names = context.resources.getStringArray(
R.array.search_engines_names
).toMutableList()
val urls = context.resources.getStringArray(
R.array.search_engines_values
).toMutableList()
// Remove the "Always ask" entry. The arrays search_engines_*
// are used in the preferences too.
names.removeAt(0)
urls.removeAt(0)
if (prefs.openWithUrl.isNotEmpty()) {
names.add(prefs.openWithUrl)
urls.add(prefs.openWithUrl)
}
val queryUri = alertDialog<String>(context) { resume ->
setTitle(R.string.pick_search_engine)
setItems(names.toTypedArray()) { _, which ->
resume(urls[which] + query.urlEncode())
}
} ?: return
view(context, queryUri, false)
fired = openUrlOrSearch(context, String(data))
}
}
private suspend fun openUrlOrSearch(
context: Context,
url: String,
search: Boolean = true
): Boolean {
if (context.openUrl(url, silent = true)) {
return true
}
if (search) {
return prependSearchUrl(context, url)?.let {
openUrlOrSearch(context, it, false)
} ?: false
}
return false
}
private suspend fun prependSearchUrl(
context: Context,
query: String
): String? {
val defaultSearchUrl = prefs.defaultSearchUrl
if (defaultSearchUrl.isNotEmpty()) {
return defaultSearchUrl + query.urlEncode()
}
val names = context.resources.getStringArray(
R.array.search_engines_names
).toMutableList()
val urls = context.resources.getStringArray(
R.array.search_engines_values
).toMutableList()
// Remove the "Always ask" entry. The arrays search_engines_*
// are used in the preferences too.
names.removeAt(0)
urls.removeAt(0)
if (prefs.openWithUrl.isNotEmpty()) {
names.add(prefs.openWithUrl)
urls.add(prefs.openWithUrl)
}
return alertDialog(context) { resume ->
setTitle(R.string.pick_search_engine)
setItems(names.toTypedArray()) { _, which ->
resume(urls[which] + query.urlEncode())
}
}
}
@@ -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))
@@ -88,6 +82,6 @@ private fun List<String>.simpleFindParse(date: String): Date? {
private fun SimpleDateFormat.simpleParse(date: String): Date? = try {
parse(date)
} catch (e: Exception) {
} catch (_: Exception) {
null
}
@@ -2,12 +2,12 @@ package de.markusfisch.android.binaryeye.actions.web
import android.content.Context
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IAction
import de.markusfisch.android.binaryeye.actions.Action
import de.markusfisch.android.binaryeye.content.openUrl
object WebAction : IAction {
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 : IAction {
if (!url.startsWith("http") && !url.startsWith("ftp")) {
url = "http://${url}"
}
context.openUrl(url)
fired = context.openUrl(url)
}
}
@@ -2,31 +2,39 @@ package de.markusfisch.android.binaryeye.actions.wifi
import android.content.Context
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.IAction
import de.markusfisch.android.binaryeye.actions.Action
import de.markusfisch.android.binaryeye.widget.toast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
object WifiAction : IAction {
object WifiAction : Action() {
override val iconResId = R.drawable.ic_action_wifi
override val titleResId = R.string.connect_to_wifi
var password: String? = null
override fun canExecuteOn(data: ByteArray): Boolean =
WifiConnector.parse(String(data)) {
password = it
} != null
override fun canExecuteOn(data: ByteArray): Boolean {
// Just check if the data can be parsed as the app should also
// show invalid configurations so users can still access them.
val inputMap = WifiConnector.parseMap(String(data)) ?: return false
password = inputMap["P"]
return !inputMap["S"].isNullOrEmpty()
}
override suspend fun execute(
context: Context,
data: ByteArray
) = withContext(Dispatchers.IO) {
val message = WifiConnector.parse(String(data))?.let {
WifiConnector.addNetwork(context, it)
} ?: R.string.wifi_config_failed
withContext(Dispatchers.Main) {
context.toast(message)
override suspend fun execute(context: Context, data: ByteArray) {
withContext(Dispatchers.IO) {
fired = WifiConnector.parse(String(data))?.let {
WifiConnector.addNetwork(context, it)
} ?: false
withContext(Dispatchers.Main) {
context.toast(
if (fired) {
R.string.wifi_added
} else {
R.string.wifi_config_failed
}
)
}
}
}
}
@@ -1,14 +1,15 @@
package de.markusfisch.android.binaryeye.actions.wifi
import android.content.Context
import android.content.Intent
import android.net.wifi.WifiConfiguration
import android.net.wifi.WifiEnterpriseConfig
import android.net.wifi.WifiManager
import android.net.wifi.WifiNetworkSuggestion
import android.os.Build
import android.support.annotation.RequiresApi
import de.markusfisch.android.binaryeye.R
import java.util.*
import android.provider.Settings
import androidx.annotation.RequiresApi
import java.util.Locale
/**
* Normal:
@@ -35,15 +36,9 @@ import java.util.*
* The fields can appear in any order. Only "S:" is required.
*/
object WifiConnector {
fun parse(
input: String,
passwordBlock: ((password: String?) -> Unit)? = null
): Any? {
fun parse(input: String): Any? {
val inputMap = parseMap(input) ?: return null
val parsedData = SimpleDataAccessor.of(inputMap) ?: return null
passwordBlock?.apply {
invoke(parsedData.password)
}
return try {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
// WifiConfiguration is deprecated in Android Q but
@@ -54,7 +49,7 @@ object WifiConnector {
} else {
WifiNetworkSuggestion.Builder().apply(parsedData)
}
} catch (e: IllegalArgumentException) {
} catch (_: IllegalArgumentException) {
// Some arguments may be out of range, e.g. SSIDs cannot
// be longer than 32 characters in some versions of Android.
// See: https://github.com/markusfisch/BinaryEye/issues/402
@@ -62,24 +57,26 @@ object WifiConnector {
}
}
fun addNetwork(context: Context, config: Any): Int {
fun addNetwork(context: Context, config: Any): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
config is WifiNetworkSuggestion.Builder
) {
context.suggestNetworkFromBuilder(config)
return true
}
val wifiManager = context.applicationContext.getSystemService(
Context.WIFI_SERVICE
) as WifiManager
// WifiConfiguration is deprecated in Android Q.
@Suppress("DEPRECATION")
return if ((config is WifiConfiguration &&
wifiManager.enableWifi() &&
wifiManager.removeOldNetwork(config) &&
wifiManager.enableNewNetwork(config)) ||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
config is WifiNetworkSuggestion.Builder &&
wifiManager.addNetworkFromBuilder(config))
) {
R.string.wifi_added
} else {
R.string.wifi_config_failed
}
return (config is WifiConfiguration &&
wifiManager.enableWifi() &&
wifiManager.removeOldNetwork(config) &&
wifiManager.enableNewNetwork(config)) ||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
config is WifiNetworkSuggestion.Builder &&
wifiManager.addNetworkFromBuilder(config))
}
internal fun parseMap(input: String): Map<String, String>? {
@@ -93,7 +90,7 @@ object WifiConnector {
input.trimAndTerminate()
)?.groupValues?.get(1)?.let { pairs ->
pairRegex.findAll(pairs).map { pair ->
pair.groupValues[1].uppercase(Locale.US) to pair.groupValues[2]
pair.groupValues[1].uppercase(Locale.US) to pair.groupValues[2].unescape
}.toMap()
}
}
@@ -103,47 +100,43 @@ object WifiConnector {
) {
// This should be private but because of testing it isn't possible.
internal val ssid: String
get() = inputMap.getValue("S").unescape
get() = inputMap.getValue("S")
internal val securityType: String
get() = inputMap["T"]?.unescape ?: ""
get() = inputMap["T"] ?: ""
internal val password: String?
get() = inputMap["P"]?.unescape
get() = inputMap["P"]
internal val hidden: Boolean
get() = inputMap["H"]?.unescape == "true"
get() = inputMap["H"] == "true"
internal val anonymousIdentity: String
get() = inputMap["AI"]?.unescape ?: inputMap["A"]?.unescape ?: ""
get() = inputMap["AI"] ?: inputMap["A"] ?: ""
internal val identity: String
get() = inputMap["I"]?.unescape ?: ""
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
}
@@ -201,7 +194,7 @@ object WifiConnector {
})
}
}
} catch (e: IllegalArgumentException) {
} catch (_: IllegalArgumentException) {
// Don't accept passphrases that aren't ASCII encodeable.
return null
}
@@ -268,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
@@ -288,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
}
@@ -313,6 +306,21 @@ private fun WifiManager.addNetworkFromBuilder(
return result == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS
}
@RequiresApi(Build.VERSION_CODES.R)
private fun Context.suggestNetworkFromBuilder(
builder: WifiNetworkSuggestion.Builder
) {
val suggestion = builder.build()
startActivity(
Intent(Settings.ACTION_WIFI_ADD_NETWORKS).apply {
putParcelableArrayListExtra(
Settings.EXTRA_WIFI_NETWORK_LIST,
arrayListOf(suggestion)
)
}
)
}
// Make sure the string ends with a semicolon.
private fun String.trimAndTerminate(): String {
var trimmed = trim()
@@ -366,7 +374,7 @@ private fun WifiManager.removeOldNetwork(
}?.networkId?.also {
removeNetwork(it)
}
} catch (e: SecurityException) {
} catch (_: SecurityException) {
// The user didn't allow ACCESS_FINE_LOCATION which is
// required to access configuredNetworks and that's fine.
}
@@ -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
}
@@ -0,0 +1,304 @@
package de.markusfisch.android.binaryeye.activity
import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
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.RadioGroup
import android.widget.Spinner
import android.widget.TextView
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 AutomatedActionsActivity : AbstractBaseActivity() {
private val actions = ArrayList<AutomatedAction>()
private lateinit var adapter: ActionsAdapter
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 = 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, _ ->
editAction(position)
}
listView.setOnItemLongClickListener { _, _, position, _ ->
confirmRemoveAction(position)
true
}
findViewById<View>(R.id.add).setOnClickListener {
editAction(null)
}
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
listView.setPaddingFromWindowInsets()
}
private fun editAction(position: Int?) {
val action = position?.let { actions[it] }
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)
val intentSection = view.findViewById<View>(R.id.intent_section)
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 ?: "")
}
AutomatedAction.Type.CustomIntent -> {
intentUriView.setText(action.intentUriTemplate ?: "")
}
}
} else {
formatView.setSelection(0)
}
fun updateTypeSections(checkedId: Int) {
val isIntent = checkedId == R.id.action_type_intent
urlSection.visibility = if (isIntent) View.VISIBLE else View.GONE
intentSection.visibility = if (isIntent) View.GONE else View.VISIBLE
}
typeGroup.setOnCheckedChangeListener { _, checkedId ->
updateTypeSections(checkedId)
}
val initialTypeId = if (
action?.type == AutomatedAction.Type.CustomIntent
) {
R.id.action_type_custom_intent
} else {
R.id.action_type_intent
}
typeGroup.check(initialTypeId)
updateTypeSections(initialTypeId)
val dialog = AlertDialog.Builder(this)
.setTitle(
if (action == null) {
R.string.automated_action_add
} else {
R.string.automated_action_edit
}
)
.setView(view)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.create()
dialog.setOnShowListener {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
addOrUpdateAction(
position,
regexView,
formatView,
values,
urlView,
typeGroup,
intentUriView,
dialog
)
}
}
dialog.show()
}
private fun addOrUpdateAction(
position: Int?,
regexView: EditText,
formatView: Spinner,
values: List<String>,
urlView: EditText,
typeGroup: RadioGroup,
intentUriView: EditText,
dialog: Dialog,
) {
val pattern = regexView.text.toString().trim()
if (pattern.isEmpty()) {
regexView.error = getString(R.string.automated_action_regex_required)
return
}
try {
Regex(pattern)
} catch (_: Exception) {
regexView.error = getString(R.string.automated_action_regex_invalid)
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()) {
urlView.error = getString(R.string.automated_action_url_required)
return
}
AutomatedAction(
pattern = pattern,
type = AutomatedAction.Type.Intent,
format = format,
urlTemplate = urlTemplate
)
} else {
val intentUriTemplate = intentUriView.text.toString().trim()
if (intentUriTemplate.isEmpty()) {
intentUriView.error = getString(
R.string.automated_action_intent_uri_required
)
return
}
if (!intentUriTemplate.startsWith(
"intent:",
ignoreCase = true
)
) {
intentUriView.error = getString(
R.string.automated_action_intent_uri_invalid
)
return
}
AutomatedAction(
pattern = pattern,
type = AutomatedAction.Type.CustomIntent,
format = format,
intentUriTemplate = intentUriTemplate
)
}
if (position == null) {
actions.add(newAction)
} else {
actions[position] = newAction
}
prefs.setAutomatedActions(actions)
adapter.notifyDataSetChanged()
dialog.dismiss()
}
private fun confirmRemoveAction(position: Int) {
AlertDialog.Builder(this)
.setMessage(R.string.automated_action_remove_confirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
actions.removeAt(position)
prefs.setAutomatedActions(actions)
adapter.notifyDataSetChanged()
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
private class ActionsAdapter(
context: Context,
private val items: List<AutomatedAction>
) : ArrayAdapter<AutomatedAction>(
context,
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 action = items[position]
getViewHolder(view).apply {
title.text = action.pattern
subtitle.text = when (action.type) {
AutomatedAction.Type.Intent -> if (!action.urlTemplate.isNullOrEmpty()) {
context.getString(
R.string.automated_action_list_intent,
action.urlTemplate
)
} else {
context.getString(R.string.automated_action_list_intent_empty)
}
AutomatedAction.Type.CustomIntent ->
if (!action.intentUriTemplate.isNullOrEmpty()) {
context.getString(
R.string.automated_action_list_custom_intent,
action.intentUriTemplate
)
} else {
context.getString(
R.string.automated_action_list_custom_intent_empty
)
}
}
action.format?.let { format ->
subtitle.text = "${subtitle.text} (${format.prettifyFormatName()})"
}
}
return view
}
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
}
private data class ViewHolder(
val title: TextView,
val subtitle: TextView,
)
}
}
@@ -1,19 +1,17 @@
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
@@ -33,6 +31,7 @@ import de.markusfisch.android.binaryeye.database.toScan
import de.markusfisch.android.binaryeye.io.addSuffixIfNotGiven
import de.markusfisch.android.binaryeye.io.toSaveResult
import de.markusfisch.android.binaryeye.io.writeExternalFile
import de.markusfisch.android.binaryeye.net.createEncodeDeeplink
import de.markusfisch.android.binaryeye.os.getScreenBrightness
import de.markusfisch.android.binaryeye.os.setScreenBrightness
import de.markusfisch.android.binaryeye.view.doOnApplyWindowInsets
@@ -54,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
}
@@ -64,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.
@@ -103,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)
@@ -127,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)
@@ -135,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<*> {
@@ -171,7 +154,7 @@ class BarcodeFragment : Fragment() {
content,
barcodeFormat,
getInt(EC_LEVEL),
getInt(MARGIN),
getBoolean(ADD_QUIET_ZONE),
colors
)
}
@@ -181,9 +164,9 @@ class BarcodeFragment : Fragment() {
super.onResume()
if (prefs.brightenScreen) {
// Post to make sure brightenScreenItem is initialized.
view?.post {
contentView?.postDelayed({
brightenScreen()
}
}, 500)
}
}
@@ -197,8 +180,8 @@ class BarcodeFragment : Fragment() {
}
}
override fun onDestroyView() {
super.onDestroyView()
override fun onDestroy() {
super.onDestroy()
parentJob.cancel()
}
@@ -215,21 +198,23 @@ 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
}
R.id.copy_to_clipboard -> {
context.apply {
copyToClipboard(barcode.text())
toast(R.string.copied_to_clipboard)
}
copyToClipboard(barcode.text())
true
}
R.id.copy_as_deeplink -> {
copyToClipboard(deeplinkToCopy())
true
}
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 ->
@@ -257,6 +242,16 @@ class BarcodeFragment : Fragment() {
}
}
private fun copyToClipboard(text: String, isSensitive: Boolean = false) {
(this as android.content.Context).copyToClipboard(text, isSensitive)
toast(R.string.copied_to_clipboard)
}
private fun deeplinkToCopy() = createEncodeDeeplink(
format = barcode.format.name,
content = barcode.textOrHex(),
)
private fun Context.pickFileType(
title: Int,
action: (FileType) -> Unit
@@ -273,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)
AlertDialog.Builder(this)
.setView(view)
.setPositiveButton(android.R.string.ok) { _, _ ->
val fileName = editText.text.toString()
@@ -329,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)
AlertDialog.Builder(this)
.setView(view)
.setPositiveButton(android.R.string.ok) { _, _ ->
write(getSize(sizeBarView.progress))
@@ -377,11 +370,12 @@ class BarcodeFragment : Fragment() {
mimeType: String,
write: (outputStream: OutputStream) -> Unit
) {
val ac = activity ?: return
scope.launch(Dispatchers.IO) {
val message = ac.writeExternalFile(fileName, mimeType, write).toSaveResult()
val message = writeExternalFile(
fileName, mimeType, write
).toSaveResult()
withContext(Dispatchers.Main) {
ac.toast(message)
toast(message)
}
}
}
@@ -418,7 +412,7 @@ class BarcodeFragment : Fragment() {
}
}
true
} catch (e: IOException) {
} catch (_: IOException) {
false
}
withContext(Dispatchers.Main) {
@@ -432,18 +426,19 @@ class BarcodeFragment : Fragment() {
}
private fun brightenScreen() {
activity?.let {
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) {
activity?.setScreenBrightness(currentBrightness)
setScreenBrightness(currentBrightness)
currentBrightness = -1f
brightenScreenItem.isChecked = false
brightenScreenItem?.isChecked = false
}
}
@@ -455,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 -> {
@@ -478,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(
@@ -530,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)
}
}
}
@@ -590,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
File diff suppressed because it is too large Load Diff
@@ -1,27 +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.net.isEncodeDeeplink
import de.markusfisch.android.binaryeye.text.unescape
import de.markusfisch.android.binaryeye.view.hideSoftKeyboard
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
@@ -30,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
@@ -40,21 +40,35 @@ 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 minMargin = 0
private var bytes: ByteArray? = null
override fun onActivityResult(
@@ -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 { prettifyFormatName(it.name) }
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,42 +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
val args = arguments
args?.getString(CONTENT_TEXT)?.let {
contentView.setText(it)
}
args?.getByteArray(CONTENT_RAW)?.let {
bytes = it
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()
}
return view
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
applyIntent(true)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@@ -235,31 +224,78 @@ class EncodeFragment : Fragment() {
super.onPause()
prefs.indexOfLastSelectedFormat = formatView.selectedItemPosition
prefs.expandEscapeSequences = unescapeCheckBox.isChecked
prefs.addQuietZone = addQuietZoneSwitch.isChecked
}
private fun applyIntent(applyDefaultFormat: Boolean) {
val args = intent?.extras ?: getExtrasFromDeeplink()
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
val format = formats[formatView.selectedItemPosition]
fragmentManager?.addFragment(
BarcodeFragment.newInstance(
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
contentView.post {
startActivity(newBarcodeIntent(content))
}
}
else -> -1
},
if (format.canBeInverted()) {
colorsSpinner.selectedItemPosition
} else 0
)
private fun newBarcodeIntent(content: Any): Intent {
val format = writeableFormats[formatView.selectedItemPosition]
return BarcodeActivity.newIntent(
this,
content,
format,
format.getErrorCorrectionLevel(ecSpinner.selectedItemPosition),
addQuietZoneSwitch.isChecked,
if (format.canBeInverted()) {
colorsSpinner.selectedItemPosition
} else 0
)
}
@@ -298,16 +334,41 @@ 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"
private const val FORMAT = "format"
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
): Fragment {
format: String? = null,
execute: Boolean = false
): Intent {
val args = Bundle()
content?.let {
when (content) {
@@ -317,11 +378,12 @@ class EncodeFragment : Fragment() {
"content must be a String or a ByteArray"
)
}
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)
}
}
}
}
@@ -342,30 +404,39 @@ 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): BarcodeFormat = try {
private fun String.toFormat(
default: BarcodeFormat = BarcodeFormat.QRCode
): BarcodeFormat = try {
BarcodeFormat.valueOf(this)
} catch (_: IllegalArgumentException) {
default
}
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,4 +1,4 @@
package de.markusfisch.android.binaryeye.fragment
package de.markusfisch.android.binaryeye.activity
import android.annotation.SuppressLint
import android.app.AlertDialog
@@ -8,24 +8,19 @@ import android.database.Cursor
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 androidx.core.content.ContextCompat
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 +33,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 +47,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,12 +64,10 @@ 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,
@@ -96,7 +88,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 +140,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 +158,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 +167,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 +185,11 @@ class HistoryFragment : Fragment() {
wipeLastShareFile()
}
override fun onResume() {
super.onResume()
update()
}
override fun onPause() {
super.onPause()
listViewState = listView.onSaveInstanceState()
@@ -235,35 +214,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 +248,7 @@ class HistoryFragment : Fragment() {
}
R.id.share_as_file -> {
context.pickListSeparatorAndShare(true)
this.pickListSeparatorAndShare(true)
true
}
@@ -300,13 +275,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 +292,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,8 +328,8 @@ class HistoryFragment : Fragment() {
private fun showScan(id: Long) = db.getScan(id)?.also { scan ->
closeActionMode()
try {
fragmentManager?.addFragment(DecodeFragment.newInstance(scan))
} catch (e: IllegalArgumentException) {
startActivity(DecodeActivity.newIntent(this, scan))
} catch (_: IllegalArgumentException) {
// Ignore, can never happen.
}
}
@@ -418,7 +402,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 &&
@@ -490,12 +474,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,171 @@
package de.markusfisch.android.binaryeye.activity
import android.app.AlertDialog
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 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 = AlertDialog.Builder(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) {
AlertDialog.Builder(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,153 +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.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?.startsWith("binaryeye://encode") == true -> {
val uri = Uri.parse(intent.dataString)
EncodeFragment.newInstance(
uri.getQueryParameter("content"),
uri.getQueryParameter("format")
)
}
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? = if (
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
) {
@Suppress("DEPRECATION")
getParcelableExtra(name)
} else {
getParcelableExtra(name, Scan::class.java)
}
@@ -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
@@ -49,15 +50,13 @@ class PickActivity : AppCompatActivity() {
tryRotate = true,
tryInvert = true,
tryDownscale = true,
returnCodabarStartEnd = true,
maxNumberOfSymbols = 1,
formats = prefs.barcodeFormats.toFormatSet()
)
private lateinit var cropImageView: CropImageView
private lateinit var detectorView: DetectorView
private lateinit var freeRotationItem: MenuItem
private var cropImageView: CropImageView? = null
private var detectorView: DetectorView? = null
private var freeRotationItem: MenuItem? = null
private var result: Result? = null
override fun attachBaseContext(base: Context?) {
@@ -74,7 +73,6 @@ class PickActivity : AppCompatActivity() {
setTitle(R.string.pick_code_to_scan)
initBars()
supportFragmentManager.addOnBackStackChangedListener {
colorSystemAndToolBars(this@PickActivity)
}
@@ -83,6 +81,9 @@ class PickActivity : AppCompatActivity() {
if (bitmap == null) {
applicationContext.toast(R.string.error_no_content)
finish()
// This is why cropImageView and detectorView aren't lateinit.
// If they were, a kotlin.UninitializedPropertyAccessException
// would happen here.
return
}
@@ -106,10 +107,10 @@ class PickActivity : AppCompatActivity() {
scanWithinBounds(bitmap)
}
setPaddingFromWindowInsets()
restoreCropHandlePos(PICKER_CROP_HANDLE)
cropHandleName = "picker_crop_handle"
}
findViewById(R.id.scan).setOnClickListener {
findViewById<View>(R.id.scan).setOnClickListener {
showResult()
}
}
@@ -129,17 +130,19 @@ class PickActivity : AppCompatActivity() {
}
private fun scanWithinBounds(bitmap: Bitmap) {
val dv = detectorView ?: return
val civ = cropImageView ?: return
val viewRoi = Rect(
if (detectorView.roi.width() < 1) {
cropImageView.getBoundsRect()
if (dv.roi.width() < 1) {
civ.getBoundsRect()
} else {
detectorView.roi
dv.roi
}
)
val mappedRect = RectF(cropImageView.mappedRect)
val imageRotation = cropImageView.imageRotation
val pivotX = cropImageView.pivotX
val pivotY = cropImageView.pivotY
val mappedRect = RectF(civ.mappedRect)
val imageRotation = civ.imageRotation
val pivotX = civ.pivotX
val pivotY = civ.pivotY
scope.launch {
val cropped = bitmap.crop(
getNormalizedRoi(mappedRect, viewRoi),
@@ -170,10 +173,10 @@ class PickActivity : AppCompatActivity() {
}
result = it
scanFeedback()
detectorView.update(
dv.update(
matrix.mapPosition(
it.position,
detectorView.coordinates
dv.coordinates
)
)
}
@@ -203,7 +206,7 @@ class PickActivity : AppCompatActivity() {
override fun onDestroy() {
super.onDestroy()
detectorView.storeCropHandlePos(PICKER_CROP_HANDLE)
detectorView?.storeCropHandlePos()
parentJob.cancel()
releaseToneGenerators()
}
@@ -225,8 +228,8 @@ class PickActivity : AppCompatActivity() {
R.id.toggle_free -> {
prefs.freeRotation = prefs.freeRotation xor true
cropImageView.freeRotation = prefs.freeRotation
freeRotationItem.updateFreeRotationIcon()
cropImageView?.freeRotation = prefs.freeRotation
freeRotationItem?.updateFreeRotationIcon()
true
}
@@ -245,7 +248,8 @@ class PickActivity : AppCompatActivity() {
}
private fun rotateClockwise() {
cropImageView.imageRotation = (cropImageView.imageRotation + 90) % 360
val civ = cropImageView ?: return
civ.imageRotation = (civ.imageRotation + 90) % 360
}
private fun loadSentImage(intent: Intent): Bitmap? {
@@ -267,10 +271,6 @@ class PickActivity : AppCompatActivity() {
applicationContext.toast(R.string.no_barcode_found)
}
}
companion object {
private const val PICKER_CROP_HANDLE = "picker_crop_handle"
}
}
private fun getNormalizedRoi(imageRect: RectF, roi: Rect): RectF {
@@ -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
}
}
@@ -0,0 +1,163 @@
package de.markusfisch.android.binaryeye.activity
import android.app.AlertDialog
import android.content.Context
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.TextView
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
class ProfilesActivity : AbstractBaseActivity() {
private val profiles = ArrayList<ProfileItem>()
private lateinit var adapter: ProfilesAdapter
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setScreenContentView(R.layout.activity_profiles)
setTitle(R.string.profile)
refreshProfiles()
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, _ ->
selectProfile(position)
}
listView.setOnItemLongClickListener { _, _, position, _ ->
confirmRemoveProfile(position)
true
}
findViewById<View>(R.id.add).setOnClickListener {
addProfile()
}
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
listView.setPaddingFromWindowInsets()
}
private fun refreshProfiles() {
profiles.clear()
profiles.add(
ProfileItem(
null,
getString(R.string.profile_default)
)
)
prefs.profiles.forEach { name ->
profiles.add(ProfileItem(name, name))
}
if (this::adapter.isInitialized) {
adapter.notifyDataSetChanged()
}
}
private fun selectProfile(position: Int) {
val profile = profiles[position].name
if (prefs.profile == profile) {
return
}
prefs.load(this, profile)
adapter.notifyDataSetChanged()
}
// Dialogs do not have a parent view.
@Suppress("InflateParams")
private fun addProfile() {
val view = layoutInflater.inflate(
R.layout.dialog_profile_name, null
)
val editText = view.findViewById<EditText>(R.id.name)
AlertDialog.Builder(this)
.setView(view)
.setPositiveButton(android.R.string.ok) { _, _ ->
val name = editText.text.toString().trim()
if (name.isNotEmpty() && prefs.addProfile(name)) {
prefs.load(this, name)
refreshProfiles()
} else {
toast(R.string.profile_invalid_name)
}
}
.show()
}
private fun confirmRemoveProfile(position: Int) {
val profile = profiles[position]
val profileName = profile.name ?: return
AlertDialog.Builder(this)
.setMessage(R.string.profile_remove_confirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
val isCurrent = prefs.profile == profileName
prefs.removeProfile(profileName)
if (isCurrent) {
prefs.load(this, null)
}
refreshProfiles()
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
private class ProfilesAdapter(
context: Context,
private val items: List<ProfileItem>
) : ArrayAdapter<ProfileItem>(
context,
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 = items[position]
val isCurrent = item.name == prefs.profile
getViewHolder(view).apply {
title.text = item.label
if (isCurrent) {
subtitle.text = context.getString(R.string.profile_current)
subtitle.visibility = View.VISIBLE
} else {
subtitle.text = ""
subtitle.visibility = View.GONE
}
}
return view
}
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
}
private data class ViewHolder(
val title: TextView,
val subtitle: TextView,
)
}
}
private data class ProfileItem(
val name: String?,
val label: String
)
@@ -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,10 +10,10 @@ 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 {
finish()
}
findViewById(R.id.simple).setOnClickListener {
findViewById<View>(R.id.simple).setOnClickListener {
prefs.apply {
showCropHandle = false
zoomBySwiping = false
@@ -21,6 +22,8 @@ class WelcomeActivity : AppCompatActivity() {
openImmediately = true
showMetaData = false
showHexDump = false
defaultSearchUrl = "https://www.google.com/search?q="
openWithUrl = defaultSearchUrl
expandEscapeSequences = false
brightenScreen = true
}
@@ -2,12 +2,16 @@ 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 androidx.core.content.ContextCompat
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.database.Database
import java.text.DateFormat
@@ -15,9 +19,14 @@ 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)
@@ -89,8 +98,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 +126,27 @@ class ScansAdapter(context: Context, cursor: Cursor) :
contentView.setCompoundDrawablesWithIntrinsicBounds(
icon, 0, 0, 0
)
formatView.text = prettifyFormatName(cursor.getString(formatIndex))
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)
@@ -125,21 +160,97 @@ class ScansAdapter(context: Context, cursor: Cursor) :
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
)
}
fun prettifyFormatName(format: String) = format.replace("_", " ")
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
"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 {
@@ -149,7 +260,7 @@ private fun formatDateTime(rfc: String): String {
DateFormat.SHORT
).format(it)
}
} catch (e: ParseException) {
} catch (_: ParseException) {
// Ignore and fall through.
}
return rfc
@@ -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
@@ -0,0 +1,111 @@
package de.markusfisch.android.binaryeye.automation
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.net.urlEncode
import org.json.JSONArray
import org.json.JSONObject
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
) {
enum class Type {
Intent, CustomIntent
}
fun matches(content: String, format: String): Boolean = try {
(this.format == null || this.format == format) &&
Regex(pattern).matches(content)
} catch (_: Exception) {
false
}
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 ?: "")
}
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"
fun fromJson(obj: JSONObject): AutomatedAction? {
val pattern = obj.optString(KEY_PATTERN, "").trim()
if (pattern.isEmpty()) {
return null
}
val type = when (obj.optString(KEY_TYPE, "")) {
"custom_intent" -> Type.CustomIntent
"intent" -> Type.Intent
"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,
""
).trim()
return AutomatedAction(
pattern = pattern,
type = type,
format = format.ifEmpty { null },
urlTemplate = urlTemplate.ifEmpty { null },
intentUriTemplate = intentUriTemplate.ifEmpty { null }
)
}
fun fromJsonArray(json: String): MutableList<AutomatedAction> {
val actions = ArrayList<AutomatedAction>()
val array = try {
JSONArray(json)
} catch (_: Exception) {
JSONArray()
}
for (i in 0 until array.length()) {
val obj = array.optJSONObject(i) ?: continue
fromJson(obj)?.let { actions.add(it) }
}
return actions
}
fun toJsonArray(actions: List<AutomatedAction>): String {
val array = JSONArray()
actions.forEach { array.put(it.toJson()) }
return array.toString()
}
}
}
fun String.buildUrl(scan: Scan): String {
return buildTemplate(scan, encodeResult = true)
}
fun String.buildTemplate(scan: Scan, encodeResult: Boolean): String {
val text = scan.text
val raw = scan.raw ?: text.toByteArray()
val result = if (encodeResult) {
text.urlEncode()
} else {
text
}
val format = if (encodeResult) {
scan.format.name.urlEncode()
} else {
scan.format.name
}
return this
.replace("{RESULT}", result)
.replace("{RESULT_BYTES}", raw.toHexString())
.replace("{FORMAT}", format)
}
@@ -0,0 +1,59 @@
package de.markusfisch.android.binaryeye.automation
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.database.Scan
import de.markusfisch.android.binaryeye.widget.toast
fun Context.runAutomatedActions(scan: Scan): Boolean {
val content = scan.text
if (content.isEmpty()) {
return false
}
val actions = prefs.automatedActions
if (actions.isEmpty()) {
return false
}
for (action in actions) {
if (action.matches(content, scan.format.name)) {
return when (action.type) {
AutomatedAction.Type.Intent -> {
val url = action.urlTemplate?.buildUrl(scan)
if (url.isNullOrEmpty()) {
toast(R.string.automated_action_invalid_url)
false
} else {
openUrl(url)
}
}
AutomatedAction.Type.CustomIntent ->
execCustomIntent(action, scan)
}
}
}
return false
}
private fun Context.execCustomIntent(
action: AutomatedAction,
scan: Scan
): Boolean {
val template = action.intentUriTemplate
if (template.isNullOrEmpty()) {
toast(R.string.automated_action_intent_uri_required)
return false
}
val intentUri = template.buildTemplate(scan, encodeResult = false)
val intent = try {
Intent.parseUri(intentUri, Intent.URI_INTENT_SCHEME)
} catch (_: Exception) {
toast(R.string.automated_action_intent_uri_invalid)
return false
}
return execShareIntent(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
@@ -24,7 +24,7 @@ fun Scan.sendBluetoothAsync(
connect(host, true)
}
val sent = if (connected) {
send(text, host, true)
send(text, host)
} else {
false
}
@@ -35,21 +35,20 @@ fun Scan.sendBluetoothAsync(
}
fun setBluetoothHosts(listPref: ListPreference) {
val devices = try {
BluetoothAdapter.getDefaultAdapter().bondedDevices
} catch (e: SecurityException) {
try {
val devices = blue.bondedDevices ?: return
listPref.entries = devices.map {
it.name
}.toTypedArray()
listPref.entryValues = devices.map {
it.address
}.toTypedArray()
listPref.callChangeListener(listPref.value)
} catch (_: SecurityException) {
// Do nothing, either the user has denied Bluetooth access
// or the permission was removed by the system. We're catching
// the exception to keep the app from crashing.
null
} ?: return
listPref.entries = devices.map {
it.name
}.toTypedArray()
listPref.entryValues = devices.map {
it.address
}.toTypedArray()
listPref.callChangeListener(listPref.value)
}
}
private var socket: BluetoothSocket? = null
@@ -71,39 +70,41 @@ private fun connect(deviceName: String, onceMore: Boolean): Boolean = try {
writer = socket?.outputStream?.writer()
isConnected = true
true
} catch (e: Exception) {
if (onceMore)
} catch (_: Exception) {
if (onceMore) {
connect(deviceName, false)
else
} else {
close()
}
false
}
private fun send(message: String, host: String, onceMore: Boolean): Boolean = try {
private fun send(message: String, host: String): Boolean = try {
writer?.apply {
write(message)
write("\n")
flush()
}
true
} catch (e: Exception) {
} catch (_: Exception) {
close()
if (connect(host, false))
send(message, host, false)
else
if (connect(host, false)) {
send(message, host)
} else {
false
}
}
private fun close() {
try {
writer?.close()
} catch (e: IOException) {
} catch (_: IOException) {
// Catch exception if writer wasn't initialized.
}
try {
socket?.close()
} catch (e: IOException) {
} catch (_: IOException) {
// Nothing we can do about this but keep
// the app from crashing.
}
@@ -113,11 +114,15 @@ private fun close() {
}
private fun findByName(findableName: String): BluetoothDevice? {
val deviceList = blue.bondedDevices
for (device in deviceList) {
if (device.address == findableName) {
return device
try {
val deviceList = blue.bondedDevices
for (device in deviceList) {
if (device.address == findableName) {
return device
}
}
} catch (_: SecurityException) {
// Fall through.
}
return null
}
@@ -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
)
}
}
@@ -61,7 +63,7 @@ fun String?.toErrorCorrectionInt() = when (this) {
else -> -1
}
abstract class Barcode<T>(
sealed class Barcode<T>(
val content: T,
val format: BarcodeFormat,
val colors: BarcodeColors
@@ -96,27 +98,36 @@ abstract class Barcode<T>(
}
protected abstract fun toText(): String
fun textOrHex(): String = when (content) {
is String -> content
is ByteArray -> content.toHexString()
else -> throw IllegalArgumentException("Illegal arguments")
}
}
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, 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
)
}
@@ -177,3 +188,31 @@ private fun BitMatrix.inflate(
}
return scaled
}
private fun BarcodeFormat.resolveSize(size: Int): Size = when (this) {
// 1D barcodes don't need as much vertical space.
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.UPCA,
BarcodeFormat.UPCE -> Size(size, size / 3)
// 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
)
}
}
@@ -1,3 +0,0 @@
package de.markusfisch.android.binaryeye.content
fun ByteArray.toHexString() = joinToString("") { "%02X".format(it) }
@@ -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()
}
}
@@ -0,0 +1,67 @@
package de.markusfisch.android.binaryeye.content
import android.content.Context
import de.markusfisch.android.binaryeye.R
private val idToLabel = hashMapOf(
"DCA" to R.string.idl_vehicle_class,
"DCB" to R.string.idl_restriction_codes,
"DCD" to R.string.idl_endorsement_codes,
"DBA" to R.string.idl_date_of_expiry,
"DCS" to R.string.idl_customer_family_name,
"DAC" to R.string.idl_customer_first_name,
"DCT" to R.string.idl_customer_first_name_alt,
"DAD" to R.string.idl_customer_middle_name,
"DBD" to R.string.idl_date_of_issue,
"DBB" to R.string.idl_date_of_birth,
"DBC" to R.string.idl_sex,
"DAY" to R.string.idl_eye_color,
"DAU" to R.string.idl_body_height,
"DAG" to R.string.idl_address_street,
"DAI" to R.string.idl_address_city,
"DAJ" to R.string.idl_address_jurisdiction_code,
"DAK" to R.string.idl_address_postal_code,
"DAQ" to R.string.idl_customer_id_number,
"DCF" to R.string.idl_document_discriminator,
"DCG" to R.string.idl_country,
"DDE" to R.string.idl_family_name_truncation,
"DDF" to R.string.idl_first_name_truncation,
"DDG" to R.string.idl_middle_name_truncation,
"DAH" to R.string.idl_address_street_2,
"DAZ" to R.string.idl_hair_color,
"DCI" to R.string.idl_place_of_birth,
"DCJ" to R.string.idl_audit_information,
"DCK" to R.string.idl_inventory_control_number,
"DBN" to R.string.idl_alias_family_name,
"DBG" to R.string.idl_alias_given_name,
"DBS" to R.string.idl_alias_suffix_name,
"DCU" to R.string.idl_name_suffix,
"DCE" to R.string.idl_weight_range,
"DCL" to R.string.idl_ethnicity,
"DCM" to R.string.idl_standard_vehicle_classification,
"DCN" to R.string.idl_standard_endorsement_code,
"DCO" to R.string.idl_standard_restriction_code,
"DCP" to R.string.idl_vehicle_classification_description,
"DCQ" to R.string.idl_endorsement_code_description,
"DCR" to R.string.idl_restriction_code_description,
"DDA" to R.string.idl_compliance_type,
"DDB" to R.string.idl_card_revision_date,
"DDC" to R.string.idl_hazmat_endorsement_expiration_date,
"DDD" to R.string.idl_limited_duration_document_indicator,
"DAW" to R.string.idl_weight_pounds,
"DAX" to R.string.idl_weight_kilograms,
"DDH" to R.string.idl_under_18_until_date,
"DDI" to R.string.idl_under_19_until_date,
"DDJ" to R.string.idl_under_21_until_date,
"DDK" to R.string.idl_organ_donor_indicator,
"DDL" to R.string.idl_veteran_indicator
)
fun Context.idlToRes(id: String): String {
val resId = idToLabel[id]
return if (resId != null) {
getString(resId)
} else {
id
}
}
@@ -0,0 +1,138 @@
package de.markusfisch.android.binaryeye.content
data class IdlInfo(
var iin: String? = null,
val elements: MutableMap<String, String> = linkedMapOf()
)
object IdlParser {
private fun resolveSex(value: String): String = when (value.firstOrNull()) {
'1' -> "M"
'2' -> "F"
'9' -> "X"
else -> value
}
private fun skipWhitespace(s: String, p: Int): Int {
var i = p
while (i < s.length && s[i].isWhitespace()) {
++i;
}
return i
}
private fun findSubType(s: String, idlInfo: IdlInfo): String {
val len = s.length
var p = skipWhitespace(s, 0)
if (p < len && s[p] == '@') {
while (p < len) {
if (!s[p].isDigit()) {
++p
continue
}
var d = p
while (d < len && s[d].isDigit()) {
++d
}
if (d - p < 8 || len - d < 3) {
p = d
continue
}
val code = s.substring(d, d + 2)
if (code != "DL" && code != "ID") {
p = d + 2
continue
}
idlInfo.elements["DL"] = code
d += 2
while (d < len) {
while (d < len && s[d] !in "DI") {
++d
}
if (len - d < 2) break
val sec = s.substring(d, d + 2)
if (sec == "DL" || sec == "ID") {
return s.substring(d + 2)
}
++d
}
break
}
}
return s
}
private fun findIIN(s: String): String? {
val ansi = s.indexOf("ANSI").takeIf { it >= 0 } ?: return null
var p = ansi + 4
val len = s.length
val whitespaceSkipped = skipWhitespace(s, p)
if (whitespaceSkipped == p) {
return null
}
p = whitespaceSkipped
val iinStart = p
var digits = 0
while (digits < 6 && p < len && s[p].isDigit()) {
++p
++digits
}
return s.substring(iinStart, p)
}
fun parse(s: String?): IdlInfo? {
if (s.isNullOrEmpty()) {
return null
}
val idlInfo = IdlInfo().apply {
iin = findIIN(s)
}
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
}
// 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)
if (key == "DBC") {
value = resolveSex(value)
}
idlInfo.elements[key] = value.trim()
}
}
skipSubtype = p < len && data[p] == '\r'
start = p + 1
}
return if (idlInfo.elements.isEmpty()) null else idlInfo
}
}
@@ -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,7 +5,8 @@ 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.widget.toast
@@ -33,12 +34,20 @@ fun Context.startIntent(intent: Intent): Boolean = try {
// necessary.
startActivity(intent)
true
} catch (e: ActivityNotFoundException) {
} catch (_: ActivityNotFoundException) {
false
}
fun Context.openUrl(url: String, silent: Boolean = false): Boolean {
val intent = Intent(Intent.ACTION_VIEW, url.parseAndNormalizeUri())
return openUri(url.parseAndNormalizeUri(), silent)
}
fun Context.openUri(uri: Uri, silent: Boolean = false): Boolean {
val intent = Intent(Intent.ACTION_VIEW, uri).apply {
if (uri.scheme == "content") {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
}
return if (silent) {
startIntent(intent)
} else {
@@ -46,12 +55,8 @@ fun Context.openUrl(url: String, silent: Boolean = false): Boolean {
}
}
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().let {
it.normalizeScheme()
}
fun Context.shareText(text: String, mimeType: String = "text/plain") {
@@ -91,7 +96,7 @@ fun <T> Context.shareAsFile(content: T, fileName: String) {
FileOutputStream(file).use {
it.write(bytes)
}
} catch (e: IOException) {
} catch (_: IOException) {
toast(R.string.error_saving_file)
return
}
@@ -2,7 +2,6 @@ package de.markusfisch.android.binaryeye.database
import android.content.Context
import android.database.Cursor
import de.markusfisch.android.binaryeye.content.toHexString
import de.markusfisch.android.binaryeye.io.writeExternalFile
import java.io.ByteArrayOutputStream
import java.io.OutputStream
@@ -30,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"
)
}
}
}
@@ -2,7 +2,6 @@ package de.markusfisch.android.binaryeye.database
import android.content.Context
import android.database.Cursor
import de.markusfisch.android.binaryeye.content.toHexString
import de.markusfisch.android.binaryeye.io.writeExternalFile
import org.json.JSONArray
import org.json.JSONException
@@ -22,6 +21,7 @@ fun Cursor.exportJson(): String? {
return null
}
val columns = arrayOf(
Database.SCANS_PINNED,
Database.SCANS_DATETIME,
Database.SCANS_FORMAT,
Database.SCANS_NAME,
@@ -70,7 +70,7 @@ private fun Cursor.toJsonObject(
obj.put(it.second, value ?: "")
}
obj
} catch (e: JSONException) {
} catch (_: JSONException) {
obj
}
}
@@ -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 {
@@ -45,7 +45,8 @@ data class Scan(
dateTime == other.dateTime &&
text == other.text &&
((raw == null && other.raw == null) ||
(raw != null && other.raw != null && raw.contentEquals(other.raw))) &&
(raw != null && other.raw != null &&
raw.contentEquals(other.raw))) &&
format == other.format &&
errorCorrectionLevel == other.errorCorrectionLevel &&
version == other.version &&
@@ -56,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
@@ -77,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 {
@@ -158,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(),
@@ -175,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,720 +0,0 @@
package de.markusfisch.android.binaryeye.fragment
import android.os.Build
import android.os.Bundle
import android.support.design.widget.FloatingActionButton
import android.support.v4.app.Fragment
import android.text.Editable
import android.text.Html
import android.text.Spannable
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.TableLayout
import android.widget.TableRow
import android.widget.TextView
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.actions.ActionRegistry
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.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.copyToClipboard
import de.markusfisch.android.binaryeye.content.shareAsFile
import de.markusfisch.android.binaryeye.content.shareText
import de.markusfisch.android.binaryeye.content.toBarcode
import de.markusfisch.android.binaryeye.content.toErrorCorrectionInt
import de.markusfisch.android.binaryeye.content.toHexString
import de.markusfisch.android.binaryeye.content.wipeLastShareFile
import de.markusfisch.android.binaryeye.database.Scan
import de.markusfisch.android.binaryeye.io.askForFileName
import de.markusfisch.android.binaryeye.io.toSaveResult
import de.markusfisch.android.binaryeye.io.writeExternalFile
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.toast
import de.markusfisch.android.zxingcpp.ZxingCpp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.security.MessageDigest
import kotlin.math.roundToInt
class DecodeFragment : Fragment() {
private lateinit var contentView: EditText
private lateinit var formatView: TextView
private lateinit var dataView: TableLayout
private lateinit var metaView: TableLayout
private lateinit var hexView: TextView
private lateinit var stampView: TextView
private lateinit var recreationView: ImageView
private lateinit var labelView: EditText
private lateinit var fab: FloatingActionButton
private lateinit var scan: Scan
private lateinit var format: String
private val parentJob = Job()
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main + parentJob)
private val dp: Float
get() = resources.displayMetrics.density
private val content: String
get() = contentView.text.toString()
private var closeAutomatically = false
private var action = ActionRegistry.DEFAULT_ACTION
private var isBinary = false
private var originalBytes: ByteArray = ByteArray(0)
private var id = 0L
private var label: String? = null
private var recreationSize = 0
override fun onCreate(state: Bundle?) {
super.onCreate(state)
setHasOptionsMenu(true)
}
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
)
closeAutomatically = prefs.closeAutomatically &&
activity?.intent?.hasExtra(MainActivity.DECODED) == true
scan = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
arguments?.getParcelable(SCAN, Scan::class.java)
} else {
@Suppress("DEPRECATION")
arguments?.getParcelable(SCAN) as Scan?
} ?: throw IllegalArgumentException("DecodeFragment needs a Scan")
isBinary = scan.text.isEmpty()
originalBytes = scan.raw ?: scan.text.toByteArray()
format = scan.format.name
id = scan.id
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)
initContentAndFab()
if (prefs.showMetaData) {
metaView.fillMetaView(scan)
} else {
metaView.visibility = View.GONE
}
if (id > 0) {
scan.label?.let {
labelView.setText(it)
label = it
}
} else {
labelView.visibility = View.GONE
}
updateViewsAndFab(scan.text, originalBytes)
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
view.findViewById<View>(R.id.scroll_view).setPaddingFromWindowInsets()
return view
}
override fun onPause() {
super.onPause()
if (id > 0) {
val newLabel = labelView.text.toString().trim()
if (newLabel != (label ?: "")) {
db.renameScan(id, newLabel)
label = newLabel
}
}
}
override fun onDestroy() {
super.onDestroy()
parentJob.cancel()
wipeLastShareFile()
}
private fun initContentAndFab() {
if (isBinary) {
contentView.setText(String(originalBytes).foldNonAlNum())
contentView.isEnabled = false
fab.setImageResource(R.drawable.ic_action_save)
fab.setOnClickListener {
askForFileNameAndSave(originalBytes)
}
} else {
contentView.setText(scan.text)
contentView.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
updateViewsAndFab(s?.toString() ?: "")
}
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) = Unit
override fun onTextChanged(
s: CharSequence?,
start: Int,
before: Int,
count: Int
) = Unit
})
fab.setOnClickListener {
executeAction(this.content)
}
if (prefs.openImmediately) {
executeAction(this.content)
backOrFinish()
}
}
}
private fun updateViewsAndFab(text: String, bytes: ByteArray? = null) {
val b = bytes ?: text.toByteArray()
resolveActionAndUpdateFab(b)
updateViews(text, b)
}
private fun resolveActionAndUpdateFab(bytes: ByteArray) {
val prevAction = action
if (!prevAction.canExecuteOn(bytes)) {
action = ActionRegistry.getAction(bytes)
}
if (prevAction !== action) {
fab.setImageResource(action.iconResId)
}
// Run this for the initial action too.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
fab.setOnLongClickListener { v ->
v.context.toast(action.titleResId)
true
}
} else {
fab.tooltipText = getString(action.titleResId)
}
}
private fun updateViews(text: String, bytes: ByteArray) {
formatView.text = resources.getQuantityString(
R.plurals.barcode_info,
bytes.size,
prettifyFormatName(format),
bytes.size
)
hexView.showIf(prefs.showHexDump) { v ->
v.text = hexDump(bytes)
}
recreationView.showIf(prefs.showRecreation) { v ->
val unmodified: Boolean
val content = if (isBinary) {
if (bytes.isEmpty()) {
clearRecreation()
return@showIf
}
unmodified = bytes.contentEquals(originalBytes)
bytes
} else {
if (text.isEmpty()) {
clearRecreation()
return@showIf
}
unmodified = text == scan.text
text
}
val barcode = if (unmodified) {
scan.toBarcode()
} else {
ContentBarcode(
content,
scan.format,
scan.errorCorrectionLevel.toErrorCorrectionInt()
)
}
try {
v.setImageBitmap(barcode.bitmap(recreationSize))
v.setOnClickListener {
fragmentManager?.addFragment(
BarcodeFragment.newInstance(barcode)
)
}
} catch (e: RuntimeException) {
clearRecreation()
}
}
dataView.fillDataView(text, bytes)
stampView.setTrackingLink(bytes, format)
}
private fun clearRecreation() {
recreationView.apply {
setImageBitmap(null)
setOnClickListener(null)
}
}
private fun TableLayout.fillDataView(text: String, bytes: ByteArray) {
val items = LinkedHashMap<Int, CharSequence?>()
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()
else -> Unit
}
if (action 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"]
)
)
}
}
fill(items)
}
private fun TableLayout.fillMetaView(scan: Scan) {
val items = linkedMapOf<Int, 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,
)
if (!scan.version.isNullOrBlank()) {
val versionString = if (scan.format.name == "QR_CODE") {
getString(
R.string.qr_version_and_modules,
scan.version,
(scan.version.toIntOrNull() ?: 0) * 4 + 17
)
} else {
scan.version
}
items.putAll(
linkedMapOf(
R.string.barcode_version_number to versionString
)
)
}
if (scan.format == ZxingCpp.BarcodeFormat.QR_CODE &&
scan.dataMask > -1
) {
items.putAll(
linkedMapOf(
R.string.qr_data_mask to scan.dataMask.toString()
)
)
}
fill(items)
}
private fun TableLayout.fill(
items: LinkedHashMap<Int, CharSequence?>
) {
removeAllViews()
visibility = if (items.isEmpty()) View.GONE else {
val ctx = context
val spaceBetween = (16f * dp).roundToInt()
items.forEach { item ->
val text = item.value
if (!text.isNullOrBlank()) {
val tr = TableRow(ctx)
val keyView = TextView(ctx)
keyView.setText(item.key)
val valueView = TextView(ctx)
valueView.setPadding(spaceBetween, 0, 0, 0)
valueView.text = text
tr.addView(keyView)
tr.addView(valueView)
addView(tr)
}
}
View.VISIBLE
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.fragment_decode, menu)
if (id > 0L) {
menu.findItem(R.id.remove).isVisible = true
}
if (action is WifiAction) {
menu.findItem(R.id.copy_password).isVisible = true
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.copy_password -> {
copyPasswordToClipboard()
maybeBackOrFinish()
true
}
R.id.copy_to_clipboard -> {
copyToClipboard(textOrHex())
maybeBackOrFinish()
true
}
R.id.share -> {
context?.apply {
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()
}
true
}
R.id.create -> {
fragmentManager?.addFragment(
EncodeFragment.newInstance(
if (isBinary) originalBytes else content,
format
)
)
true
}
R.id.remove -> {
db.removeScan(id)
backOrFinish()
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun textOrHex() = if (isBinary) {
originalBytes.toHexString()
} else {
content
}
private fun copyPasswordToClipboard() {
val ac = action
if (ac is WifiAction) {
ac.password?.let { password ->
activity?.apply {
copyToClipboard(password, isSensitive = true)
toast(R.string.copied_password_to_clipboard)
}
}
}
}
private fun copyToClipboard(text: String, isSensitive: Boolean = false) {
activity?.apply {
copyToClipboard(text, isSensitive)
toast(R.string.copied_to_clipboard)
}
}
private fun maybeBackOrFinish() {
if (closeAutomatically) {
backOrFinish()
}
}
private fun backOrFinish() {
val fm = fragmentManager
if (fm != null && fm.backStackEntryCount > 0) {
fm.popBackStack()
} else {
activity?.finish()
}
}
private fun executeAction(content: String) {
val ac = activity ?: return
if (content.isNotEmpty()) {
if (action is WifiAction &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
!ac.hasLocationPermission { executeAction(content) }
) {
return
}
scope.launch {
action.execute(ac, content.toByteArray())
maybeBackOrFinish()
}
}
}
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) }
) {
return
}
scope.launch(Dispatchers.Main) {
val name = ac.askForFileName() ?: return@launch
withContext(Dispatchers.IO) {
val message = ac.writeExternalFile(
name,
"application/octet-stream"
) {
it.write(raw)
}.toSaveResult()
withContext(Dispatchers.Main) {
ac.toast(message)
}
}
}
}
companion object {
private const val SCAN = "scan"
fun newInstance(scan: Scan): Fragment {
val args = Bundle()
args.putParcelable(SCAN, scan)
val fragment = DecodeFragment()
fragment.arguments = args
return fragment
}
}
}
private inline fun <T : View> T.showIf(
visible: Boolean,
block: (T) -> Unit
) {
visibility = if (visible) {
block.invoke(this)
View.VISIBLE
} else {
View.GONE
}
}
private val nonAlNum = "[^a-zA-Z0-9]".toRegex()
private val multipleDots = "…+".toRegex()
private fun String.foldNonAlNum() = replace(nonAlNum, "")
.replace(multipleDots, "")
private fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): String {
if (charsPerLine < 4 || bytes.isEmpty()) {
return ""
}
val dump = StringBuilder()
val hex = StringBuilder()
val ascii = StringBuilder()
val itemsPerLine = (charsPerLine - 1) / 4
val len = bytes.size
var i = 0
while (true) {
val ord = bytes[i]
hex.append(String.format("%02X ", ord))
ascii.append(if (ord > 31) ord.toInt().toChar() else " ")
++i
val posInLine = i % itemsPerLine
val atEnd = i >= len
var atLineEnd = posInLine == 0
if (atEnd && !atLineEnd) {
for (j in posInLine until itemsPerLine) {
hex.append(" ")
}
atLineEnd = true
}
if (atLineEnd) {
dump.append(hex.toString())
dump.append(" ")
dump.append(ascii.toString())
dump.append("\n")
hex.setLength(0)
ascii.setLength(0)
}
if (atEnd) {
break
}
}
return dump.toString()
}
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) {
text = trackingLink.fromHtml()
isClickable = true
movementMethod = LinkMovementMethod.getInstance()
} else {
visibility = View.GONE
}
}
private fun generateDpTrackingLink(bytes: ByteArray, format: String): String? {
// Check for Deutsche Post Matrixcode stamp.
var isStamp = false
var rawData = bytes
if (format == "DATA_MATRIX" &&
bytes.toString(Charsets.ISO_8859_1).startsWith("DEA5")
) {
if (bytes.size == 47) {
isStamp = true
} else if (bytes.size > 47) {
// Transform back to original data.
rawData = bytes.toString(Charsets.UTF_8).toByteArray(
Charsets.ISO_8859_1
)
if (rawData.size == 47) {
isStamp = true
}
}
}
if (!isStamp) {
return null
}
val hex = StringBuilder()
hex.append(String.format("%02X", rawData[9]))
hex.append(String.format("%02X", rawData[10]))
hex.append(String.format("%02X", rawData[11]))
hex.append(String.format("%02X", rawData[12]))
hex.append(String.format("%02X", rawData[13]))
hex.append(String.format("%X", (rawData[4].toInt() and 0x0f).toByte()))
hex.append(String.format("%02X", rawData[5]))
hex.append(String.format("%02X", rawData[6]))
hex.append(String.format("%02X", rawData[7]))
hex.append(String.format("%02X", rawData[8]))
val hexString = hex.toString()
val trackingNumber = hexString + String.format(
"%X",
crc4(hexString.toByteArray(Charsets.ISO_8859_1))
)
return "<a href=\"https://www.deutschepost.de/de/s/sendungsverfolgung.html?piececode=$trackingNumber\">Deutsche Post: $trackingNumber</a>"
}
// CRC-4 with polynomial x^4 + x + 1.
private fun crc4(input: ByteArray): Int {
var crc = 0
var i = 0
while (i < input.size) {
val c = input[i].toInt()
var j = 0x80
while (j != 0) {
var bit = crc and 0x8
crc = crc shl 1
if (c and j != 0) {
bit = bit xor 0x8
}
if (bit != 0) {
crc = crc xor 0x3
}
j = j ushr 1
}
++i
}
crc = crc and 0xF
return crc
}
private fun String.fromHtml() = if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
) {
Html.fromHtml(this, Html.FROM_HTML_MODE_LEGACY)
} else {
@Suppress("DEPRECATION")
Html.fromHtml(this)
}
private fun ByteArray.md5(): ByteArray = MessageDigest.getInstance("MD5").run {
update(this@md5)
digest()
}
private fun ByteArray.sha1(): ByteArray = MessageDigest.getInstance("SHA-1").run {
update(this@sha1)
digest()
}
private fun ByteArray.sha256(): ByteArray = MessageDigest.getInstance("SHA-256").run {
update(this@sha256)
digest()
}
private fun String.fold(): CharSequence {
val l = length
return if (l > 10) {
val half = l / 2
val s = substring(0, half) + "\n" + substring(half)
SpannableString(s).apply {
setSpan(
TypefaceSpan("monospace"),
0, s.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
} else {
this
}
}
@@ -9,15 +9,19 @@ 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 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
@@ -28,58 +32,119 @@ import de.markusfisch.android.binaryeye.view.systemBarRecyclerViewScrollListener
import de.markusfisch.android.binaryeye.widget.toast
class PreferencesFragment : PreferenceFragmentCompat() {
private var lastProfile: String? = null
private val changeListener = object : OnSharedPreferenceChangeListener {
override fun onSharedPreferenceChanged(
sharedPreferences: SharedPreferences?,
key: String?
) {
key ?: return
val preference = findPreference(key) ?: return
prefs.update()
when (preference.key) {
"custom_locale" -> activity?.restartApp()
"beep_tone_name" -> {
beepConfirm()
setSummary(preference)
}
"send_scan_bluetooth" -> {
if (prefs.sendScanBluetooth &&
activity?.hasBluetoothPermission() == false
) {
prefs.sendScanBluetooth = false
}
setSummary(preference)
}
else -> setSummary(preference)
val preference = findPreference<Preference>(key) ?: return
if (preference.key != PROFILE) {
prefs.update()
}
when (preference.key) {
"custom_locale" -> {
activity?.restartApp()
return
}
"beep_tone_name",
"beep_stream_name" -> beepConfirm()
"send_scan_bluetooth" -> if (
prefs.sendScanBluetooth &&
activity?.hasBluetoothPermission() == false
) {
prefs.sendScanBluetooth = false
}
}
setSummary(preference)
}
}
override fun onCreatePreferences(state: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences)
loadPreferences(rootKey)
}
private fun loadPreferences(rootKey: String? = null) {
preferenceScreen?.sharedPreferences
?.unregisterOnSharedPreferenceChangeListener(changeListener)
preferenceManager.apply {
sharedPreferencesName = prefs.profile ?: "${context.packageName}_preferences"
sharedPreferencesMode = Context.MODE_PRIVATE
}
// Refresh the preferences.
preferenceScreen = null
setPreferencesFromResource(R.xml.preferences, rootKey)
preferenceScreen.sharedPreferences
?.registerOnSharedPreferenceChangeListener(changeListener)
setSummaries(preferenceScreen)
setIcons()
wireProfiles()
wireAutomatedActions()
wireIgnoreCodes()
setBluetoothResources()
wireClearNetworkPreferences()
}
private fun wireProfiles() {
findPreference<Preference>(PROFILE)?.apply {
updateProfileSummary(this)
onPreferenceClickListener = Preference.OnPreferenceClickListener {
startActivity(Intent(activity, ProfilesActivity::class.java))
true
}
}
}
private fun wireAutomatedActions() {
findPreference<Preference>(AUTOMATED_ACTIONS)?.apply {
updateAutomatedActionsSummary(this)
onPreferenceClickListener = Preference.OnPreferenceClickListener {
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
}
}
}
private fun setBluetoothResources() {
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 {
fragmentManager.addFragment(NetworkSuggestionsFragment())
startActivity(
Intent(activity, NetworkSuggestionsActivity::class.java)
)
true
}
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
@@ -88,7 +153,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 {
@@ -124,24 +189,33 @@ class PreferencesFragment : PreferenceFragmentCompat() {
override fun onResume() {
super.onResume()
if (lastProfile != prefs.profile) {
loadPreferences()
}
activity?.setTitle(R.string.preferences)
findPreference<Preference>(AUTOMATED_ACTIONS)?.let {
updateAutomatedActionsSummary(it)
}
findPreference<Preference>(IGNORE_CODES)?.let {
updateIgnoreCodesSummary(it)
}
findPreference<Preference>(PROFILE)?.let {
updateProfileSummary(it)
}
listView.setPaddingFromWindowInsets()
listView.removeOnScrollListener(systemBarRecyclerViewScrollListener)
listView.addOnScrollListener(systemBarRecyclerViewScrollListener)
preferenceScreen.sharedPreferences
.registerOnSharedPreferenceChangeListener(changeListener)
setSummaries(preferenceScreen)
}
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)
@@ -165,38 +239,124 @@ class PreferencesFragment : PreferenceFragmentCompat() {
}
private fun setSummary(preference: Preference) {
when (preference) {
is UrlPreference -> {
preference.setSummary(preference.getUrl())
}
is ListPreference -> {
preference.setSummary(preference.entry)
}
is MultiSelectListPreference -> {
preference.setSummary(
preference.values.joinToString(", ") {
it.replace(Regex("_"), " ")
}
)
}
is PreferenceGroup -> {
setSummaries(preference)
}
when (preference.key) {
AUTOMATED_ACTIONS -> return updateAutomatedActionsSummary(preference)
IGNORE_CODES -> return updateIgnoreCodesSummary(preference)
}
when (preference) {
is UrlPreference -> preference.summary = preference.getUrl()
is ListPreference -> preference.setSummary(preference.entry)
is MultiSelectListPreference -> preference.summary =
getSortedSummary(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("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)
}
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) {
getString(R.string.automated_actions_none)
} else {
resources.getQuantityString(
R.plurals.automated_actions_count,
count,
count
)
}
}
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)
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"
@@ -80,10 +80,5 @@ private fun completeUrl(template: String): String {
if (!s.startsWith("http")) {
s = "http://${s}"
}
if (prefs.sendScanType == "0" &&
!s.matches(".*/[a-zA-Z._-]*\\?[a-zA-Z0-9&=_-]+=$".toRegex())
) {
s = "${s}?content="
}
return s
}
@@ -25,7 +25,7 @@ fun ContentResolver.loadImageUri(uri: Uri): Bitmap? = try {
openInputStream(uri)?.use {
BitmapFactory.decodeStream(it, null, options)
}
} catch (e: IOException) {
} catch (_: IOException) {
null
}
@@ -3,7 +3,7 @@ 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 {
@@ -5,26 +5,22 @@ 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))
fun Resources.getDrawableCompat(
resId: Int
): Drawable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getDrawable(resId, null)
} else {
@Suppress("DEPRECATION")
getDrawable(resId)
}
): Bitmap = getBitmapFromDrawable(
ResourcesCompat.getDrawable(this, resId, null) ?: 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
@@ -10,7 +10,9 @@ data class FrameMetrics(
var width: Int = 0,
var height: Int = 0,
var orientation: Int = 0
)
) {
fun isValid() = width > 0 && height > 0
}
fun Rect.setFrameRoi(
frameMetrics: FrameMetrics,
@@ -38,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(
@@ -86,3 +97,14 @@ fun Matrix.mapPosition(
mapPoints(coords)
return i
}
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
@@ -53,7 +53,7 @@ fun Context.writeExternalFile(
): Boolean = try {
openExternalOutputStream(fileName, mimeType).use { write(it) }
true
} catch (e: IOException) {
} catch (_: IOException) {
false
}
@@ -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
}
@@ -0,0 +1,30 @@
package de.markusfisch.android.binaryeye.net
import android.net.Uri
private const val SCHEME = "binaryeye"
private const val HOSTNAME = "markusfisch.de"
private const val ENCODE_PATH = "encode"
fun isScanDeeplink(text: String) = listOf(
"$SCHEME://scan",
"http://$HOSTNAME/BinaryEye",
"https://$HOSTNAME/BinaryEye"
).any { text.startsWith(it) }
fun isEncodeDeeplink(text: String) = listOf(
"$SCHEME://$ENCODE_PATH",
"http://$HOSTNAME/$ENCODE_PATH",
"https://$HOSTNAME/$ENCODE_PATH"
).any { text.startsWith(it) }
fun createEncodeDeeplink(format: String, content: String): String {
return Uri.Builder()
.scheme("https")
.authority(HOSTNAME)
.appendPath(ENCODE_PATH)
.appendQueryParameter("format", format)
.appendQueryParameter("content", content)
.build()
.toString()
}
@@ -1,6 +1,5 @@
package de.markusfisch.android.binaryeye.net
import de.markusfisch.android.binaryeye.content.toHexString
import de.markusfisch.android.binaryeye.database.Scan
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -36,7 +35,10 @@ private fun Scan.send(url: String, type: String): Response {
"2" -> request(url) { con ->
con.requestMethod = "POST"
con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
con.setRequestProperty(
"Content-Type",
"application/x-www-form-urlencoded"
)
con.outputStream.apply {
write(asUrlArguments().toByteArray())
close()
@@ -5,6 +5,6 @@ import java.net.URLEncoder
fun String.urlEncode(): String = try {
URLEncoder.encode(this, "UTF-8")
} catch (e: UnsupportedEncodingException) {
} catch (_: UnsupportedEncodingException) {
this
}
}
@@ -5,7 +5,9 @@ import android.view.WindowManager
fun Activity.getScreenBrightness(): Float {
val layoutParams = window.attributes
return if (layoutParams.screenBrightness == WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE) {
return if (layoutParams.screenBrightness ==
WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
) {
android.provider.Settings.System.getInt(
contentResolver,
android.provider.Settings.System.SCREEN_BRIGHTNESS
@@ -1,41 +1,49 @@
package de.markusfisch.android.binaryeye.preference
import android.content.SharedPreferences
import android.graphics.Point
import androidx.core.content.edit
import org.json.JSONObject
data class CropHandle(
val x: Int = -2, // -2 means set default roi.
val y: Int = -2,
val orientation: Int = 0
)
fun SharedPreferences.storeCropHandle(
name: String,
cropHandle: CropHandle
viewWidth: Int,
viewHeight: Int,
cropHandle: Point
) {
edit().putString(
name,
JSONObject().apply {
put("x", cropHandle.x)
put("y", cropHandle.y)
put("orientation", cropHandle.orientation)
}.toString()
).apply()
edit {
putString(
cropHandleKey(name, viewWidth, viewHeight),
JSONObject().apply {
put("x", cropHandle.x)
put("y", cropHandle.y)
}.toString()
)
}
}
fun SharedPreferences.restoreCropHandle(
name: String,
default: CropHandle = CropHandle()
): CropHandle {
val s = getString(name, null) ?: return default
viewWidth: Int,
viewHeight: Int,
default: Point = Point(-2, -2) // -2 means set default roi.
): Point {
val s = getString(
cropHandleKey(name, viewWidth, viewHeight), null
) ?: return default
return try {
val json = JSONObject(s)
CropHandle(
Point(
json.getInt("x"),
json.getInt("y"),
json.getInt("orientation")
json.getInt("y")
)
} catch (e: Exception) {
} catch (_: Exception) {
return default
}
}
private fun cropHandleKey(
name: String,
viewWidth: Int,
viewHeight: Int
) = "${name}:${viewWidth}:${viewHeight}"
@@ -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()
}
}
}
@@ -3,39 +3,59 @@ package de.markusfisch.android.binaryeye.preference
import android.annotation.SuppressLint
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 de.markusfisch.android.binaryeye.activity.CameraActivity
import androidx.core.content.edit
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 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
lateinit var preferences: SharedPreferences
val profiles = ArrayList<String>()
var profile: String? = null
set(value) {
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.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
@@ -45,6 +65,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)
@@ -90,6 +115,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)
@@ -100,6 +130,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)
@@ -197,6 +229,11 @@ class Preferences {
apply(EXPAND_ESCAPE_SEQUENCES, value)
field = value
}
var addQuietZone = true
set(value) {
apply(ADD_QUIET_ZONE, value)
field = value
}
var brightenScreen = false
set(value) {
apply(BRIGHTEN_SCREEN, value)
@@ -207,29 +244,97 @@ class Preferences {
apply(PREVIEW_SCALE, value)
field = value
}
var automatedActions = mutableListOf<AutomatedAction>()
private set
fun init(context: Context) {
preferences = PreferenceManager.getDefaultSharedPreferences(context)
// I'm not including a support library just to get the
// default preferences. Dependencies are a burden.
@Suppress("DEPRECATION")
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context)
loadProfiles()
load(context, defaultPreferences.getString(PROFILE, null))
}
fun load(context: Context, profileName: String?) {
preferences = if (profileName.isNullOrEmpty()) {
profile = null
defaultPreferences
} else {
profile = profileName
context.getSharedPreferences(
profileName,
Context.MODE_PRIVATE
)
}
update()
}
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 addProfile(name: String): Boolean {
if (!name.matches("^[a-zA-Z0-9_-]+$".toRegex()) ||
profiles.contains(name)
) {
return false
}
profiles.add(name)
saveProfiles()
return true
}
migrateCropHandle()
fun removeProfile(name: String): Boolean {
if (!profiles.remove(name)) {
return false
}
saveProfiles()
return true
}
fun saveProfiles() {
defaultPreferences.edit {
putString(PROFILES, org.json.JSONArray(profiles).toString())
}
}
fun loadProfiles() {
profiles.clear()
org.json.JSONArray(
defaultPreferences.getString(PROFILES, "[]")
).let {
profiles.addAll(Array(it.length()) { i -> it.getString(i) })
}
}
fun update() {
preferences.getStringSet(BARCODE_FORMATS, barcodeFormats)?.let {
val migrated = migrateCode39ToCode39Std(migrateBarcodeFormats(it))
if (migrated != it) {
apply(BARCODE_FORMATS, migrated)
}
barcodeFormats = addFormatsOnUpdate(
migrated,
BarcodeFormat.Code39Std,
BarcodeFormat.Code39Ext,
BarcodeFormat.Code32,
BarcodeFormat.PZN,
BarcodeFormat.DataBarOmni,
BarcodeFormat.DataBarStk,
BarcodeFormat.DataBarLtd,
BarcodeFormat.DataBarExp,
BarcodeFormat.DataBarExpStk,
BarcodeFormat.RMQRCode,
BarcodeFormat.DXFilmEdge
)
}
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)
@@ -247,18 +352,34 @@ 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"
}
preferences.getString(IGNORE_DUPLICATES_NAME, ignoreDuplicatesName)?.also {
preferences.getString(
IGNORE_DUPLICATES_NAME,
ignoreDuplicatesName
)?.also {
ignoreDuplicatesName = it
}
ignoreCodes = ignoreCodesFromJsonArray(
preferences.getString(
IGNORE_CODES,
ignoreCodesToJsonArray(
listOf(IgnoreCode(DEFAULT_IGNORE_CODE_PATTERN))
)
) ?: "[]"
)
copyImmediately = preferences.getBoolean(
COPY_IMMEDIATELY,
copyImmediately
@@ -319,6 +440,7 @@ class Preferences {
EXPAND_ESCAPE_SEQUENCES,
expandEscapeSequences
)
addQuietZone = preferences.getBoolean(ADD_QUIET_ZONE, addQuietZone)
brightenScreen = preferences.getBoolean(
BRIGHTEN_SCREEN,
brightenScreen
@@ -327,8 +449,24 @@ class Preferences {
PREVIEW_SCALE,
previewScale
)
automatedActions = fromJsonArray(
preferences.getString(AUTOMATED_ACTIONS, "[]") ?: "[]"
)
}
fun setAutomatedActions(actions: List<AutomatedAction>) {
automatedActions = actions.toMutableList()
apply(AUTOMATED_ACTIONS, toJsonArray(actions))
}
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) }
private fun addFormatsOnUpdate(
restored: Set<String>,
vararg formats: BarcodeFormat
@@ -336,36 +474,44 @@ class Preferences {
for (format in formats) {
val name = "${format.name}_added"
if (!preferences.getBoolean(name, false)) {
preferences.edit().putBoolean(name, true).apply()
preferences.edit {
putBoolean(name, true)
}
add(format.name)
}
}
}
private fun migrateCropHandle() {
val cropHandleXName = "crop_handle_x"
val def = CropHandle()
if (preferences.getInt(cropHandleXName, def.x) == def.x) {
return;
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 migrateBarcodeFormats(formats: Set<String>): Set<String> {
return formats.mapTo(mutableSetOf()) { format ->
format.migrateBarcodeFormatName()
}
storeCropHandle(
CameraActivity.CAMERA_CROP_HANDLE,
CropHandle(
preferences.getInt(cropHandleXName, def.x),
preferences.getInt("crop_handle_y", def.y),
preferences.getInt("crop_handle_orientation", def.orientation)
)
)
preferences.edit().putInt(cropHandleXName, def.x).apply()
}
fun restoreCropHandle(
name: String
) = preferences.restoreCropHandle(name)
name: String,
viewWidth: Int,
viewHeight: Int
) = preferences.restoreCropHandle(name, viewWidth, viewHeight)
fun storeCropHandle(name: String, cropHandle: CropHandle) {
preferences.storeCropHandle(name, cropHandle)
}
fun storeCropHandle(
name: String,
viewWidth: Int,
viewHeight: Int,
cropHandle: Point
) = preferences.storeCropHandle(name, viewWidth, viewHeight, cropHandle)
fun beepTone() = when (beepToneName) {
"tone_cdma_confirm" -> ToneGenerator.TONE_CDMA_CONFIRM
@@ -376,39 +522,52 @@ 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 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 {
@@ -416,8 +575,11 @@ class Preferences {
Consecutive, Any, Never
}
private const val PROFILES = "profiles"
private const val PROFILE = "profile"
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"
@@ -427,8 +589,10 @@ 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 COPY_IMMEDIATELY = "copy_immediately"
private const val SHOW_META_DATA = "show_meta_data"
@@ -448,7 +612,10 @@ class Preferences {
private const val INDEX_OF_LAST_SELECTED_EC_LEVEL = "index_of_last_selected_ec_level"
private const val FREE_ROTATION = "free_rotation"
private const val EXPAND_ESCAPE_SEQUENCES = "expand_escape_sequences"
private const val ADD_QUIET_ZONE = "add_quiet_zone"
private const val BRIGHTEN_SCREEN = "brighten_screen"
private const val PREVIEW_SCALE = "preview_scale"
private const val AUTOMATED_ACTIONS = "automated_actions"
private const val DEFAULT_IGNORE_CODE_PATTERN = "^FIDO://.*"
}
}
@@ -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)
@@ -2,16 +2,16 @@ package de.markusfisch.android.binaryeye.view
import android.content.Context
import android.content.ContextWrapper
import android.graphics.drawable.ColorDrawable
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.content.ContextCompat
import androidx.core.graphics.drawable.toDrawable
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import de.markusfisch.android.binaryeye.R
val systemBarListViewScrollListener = object : AbsListView.OnScrollListener {
@@ -26,7 +26,8 @@ val systemBarListViewScrollListener = object : AbsListView.OnScrollListener {
view.post {
val scrolled = firstVisibleItem > 0 ||
(totalItemCount > 0 && firstChildScrolled(view))
val scrollable = scrolled || totalItemCount > 0 && lastChildOutOfView(view)
val scrollable = scrolled ||
totalItemCount > 0 && lastChildOutOfView(view)
colorSystemAndToolBars(view.context, scrolled, scrollable)
}
}
@@ -38,9 +39,11 @@ val systemBarListViewScrollListener = object : AbsListView.OnScrollListener {
val systemBarRecyclerViewScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val scrolled = layoutManager.findFirstCompletelyVisibleItemPosition() != 0
val scrollable = scrolled || layoutManager.findLastVisibleItemPosition() <
recyclerView.adapter.itemCount - 1
val scrolled =
layoutManager.findFirstCompletelyVisibleItemPosition() != 0
val scrollable = scrolled ||
layoutManager.findLastVisibleItemPosition() <
(recyclerView.adapter?.itemCount ?: 0) - 1
colorSystemAndToolBars(recyclerView.context, scrolled, scrollable)
}
@@ -59,23 +62,21 @@ 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 {
findViewById<View>(R.id.navbar)?.apply {
setBackgroundColor(translucentPrimaryColor)
}
)
@@ -91,7 +92,6 @@ fun unlockStatusBarColor() {
}
private var translucentPrimaryColor = 0
private val actionBarBackground = ColorDrawable()
fun colorSystemAndToolBars(
context: Context,
scrolled: Boolean = false,
@@ -105,40 +105,29 @@ fun colorSystemAndToolBars(
}
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
}
}
}
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,15 +2,18 @@ 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(mainLayout: View, toolbar: Toolbar, navbar: View?) {
fun setPaddingFromWindowInsets(
mainLayout: View,
toolbar: Toolbar,
navbar: View?
) {
toolbarHeight = toolbar.layoutParams.height
// SDK 35+ is edge-to-edge by default, so the toolbar needs to extend
// below the status bar.
@@ -49,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(
@@ -71,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)
@@ -27,7 +27,8 @@ class ConfinedScrollView : ScrollView {
// not putting it on the queue makes it only work sometimes.
post {
getChildAt(0)?.also { child ->
scrollable = height < child.height + paddingTop + paddingBottom
scrollable = height <
child.height + paddingTop + paddingBottom
colorSystemAndToolBars(
context,
scrollY > 0,
@@ -9,17 +9,14 @@ import android.graphics.Point
import android.graphics.Rect
import android.graphics.Region
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import android.support.v4.content.ContextCompat
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.graphics.withSave
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 de.markusfisch.android.binaryeye.preference.CropHandle
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
@@ -29,26 +26,31 @@ class DetectorView : View {
val coordinates = FloatArray(32)
val roi = Rect()
var cropHandleName: String? = null
var onRoiChange: (() -> Unit)? = null
var onRoiChanged: (() -> Unit)? = null
private val currentOrientation = resources.configuration.orientation
private val invalidateRunnable: Runnable = Runnable {
coordinatesLast = 0
invalidate()
}
private val roiPaint = context.getDashedBorderPaint()
private val dotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = ContextCompat.getColor(context, R.color.dot)
style = Paint.Style.FILL
}
private val crosshairPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = ContextCompat.getColor(context, R.color.dot)
style = Paint.Style.STROKE
strokeWidth = context.resources.displayMetrics.density
}
private val dotRadius: Float
private val handleBitmap = resources.getBitmapFromDrawable(
R.drawable.button_crop
)
private val handleXRadius = handleBitmap.width / 2
private val handleYRadius = handleBitmap.height / 2
private val handlePos = Point(-1, -1)
private val inactiveHandlePos = Point(-1, -1)
private val handlePos = Point(inactiveHandlePos)
private val handleHome = Point()
private val center = Point()
private val touchDown = Point()
@@ -58,6 +60,8 @@ class DetectorView : View {
private val fabHeight: Int
private val padding: Int
private var viewWidth = 0
private var viewHeight = 0
private var coordinatesLast = 0
private var handleGrabbed = false
private var handleActive = false
@@ -84,43 +88,38 @@ class DetectorView : View {
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) :
super(context, attrs, defStyleAttr)
fun storeCropHandlePos(name: String) {
val pos = getCropHandlePos()
fun storeCropHandlePos() {
val name = cropHandleName ?: return
val pos = if (handleActive) {
handlePos
} else {
inactiveHandlePos
}
prefs.storeCropHandle(
name,
CropHandle(
pos.x,
pos.y,
currentOrientation
)
viewWidth,
viewHeight,
pos
)
}
private fun getCropHandlePos() = if (handleActive) {
handlePos
} else {
Point(-1, -1)
}
fun restoreCropHandlePos(name: String) {
val cropHandle = prefs.restoreCropHandle(name)
setCropHandlePos(
cropHandle.x,
cropHandle.y,
cropHandle.orientation
)
}
private fun setCropHandlePos(x: Int, y: Int, orientation: Int) {
// Always set handlePos even if it's invalid because
// handlePos.x may be -2 which is a signal to set the
// default ROI.
if (orientation == currentOrientation) {
handlePos.set(x, y)
} else {
handlePos.set(y, x)
fun updateCropHandlePos() {
val name = cropHandleName ?: return
if (viewWidth < 1 || viewHeight < 1) {
return
}
val cropHandle = prefs.restoreCropHandle(name, viewWidth, viewHeight)
if (cropHandle.x == -2) {
setHandleToDefaultRoi()
} else {
handlePos.set(cropHandle.x, cropHandle.y)
handleActive = handlePos.x > -1
}
if (handleActive) {
updateClipRect()
} else {
reset()
}
handleActive = handlePos.x > -1
}
fun update(numberOfCoordinates: Int) {
@@ -130,31 +129,6 @@ class DetectorView : View {
postDelayed(invalidateRunnable, 500)
}
override fun onSaveInstanceState(): Parcelable? {
if (!handleActive) {
return super.onSaveInstanceState()
}
return SavedState(super.onSaveInstanceState()).apply {
savedHandlePos.set(getCropHandlePos())
savedOrientation = currentOrientation
}
}
override fun onRestoreInstanceState(state: Parcelable) {
super.onRestoreInstanceState(
if (state is SavedState) {
setCropHandlePos(
state.savedHandlePos.x,
state.savedHandlePos.y,
state.savedOrientation
)
state.superState
} else {
state
}
)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
event ?: return super.onTouchEvent(event)
@@ -251,40 +225,48 @@ class DetectorView : View {
roi.set(0, 0, 0, 0)
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
override fun onLayout(
changed: Boolean,
left: Int,
top: Int,
right: Int,
bottom: Int
) {
super.onLayout(changed, left, top, right, bottom)
val width = right - left
val height = bottom - top
viewWidth = right - left
viewHeight = bottom - top
center.set(
left + (width / 2),
top + (height / 2)
left + (viewWidth / 2),
top + (viewHeight / 2)
)
minY = padding * 2
maxY = height - minY
maxY = viewHeight - minY
handleHome.set(
width - handleXRadius - paddingRight - padding,
height - handleYRadius - paddingBottom - fabHeight
viewWidth - handleXRadius - paddingRight - padding,
viewHeight - handleYRadius - paddingBottom - fabHeight
)
if (handlePos.x == -2) {
setHandleToDefaultRoi()
}
if (handleActive) {
updateClipRect()
} else {
reset()
}
updateCropHandlePos()
}
override fun onDraw(canvas: Canvas) {
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) {
@@ -301,10 +283,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(),
@@ -316,9 +296,6 @@ class DetectorView : View {
)
)
drawColor(shadeColor)
restore()
} else {
drawRect(roi, roiPaint)
}
}
@@ -331,6 +308,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)
@@ -356,37 +355,6 @@ class DetectorView : View {
private fun clampX(x: Int) = min(center.x * 2, max(0, x))
private fun clampY(y: Int) = min(maxY, max(minY, y))
internal class SavedState : BaseSavedState {
val savedHandlePos = Point()
var savedOrientation = 0
constructor(superState: Parcelable?) : super(superState)
private constructor(parcel: Parcel) : super(parcel) {
savedHandlePos.set(
parcel.readInt(),
parcel.readInt()
)
savedOrientation = parcel.readInt()
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeInt(savedHandlePos.x)
out.writeInt(savedHandlePos.y)
out.writeInt(savedOrientation)
}
companion object {
@JvmField
val CREATOR = object : Parcelable.Creator<SavedState> {
override fun createFromParcel(source: Parcel) = SavedState(source)
override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size)
}
}
}
}
private fun distSq(a: Point, b: Point): Int {
@@ -0,0 +1,36 @@
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_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",
"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
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#FFFFFF" 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="#FFFFFF" 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="#FFFFFF" 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>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" 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="#FFFFFF" 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="#FFFFFF" 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>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M640 680l-57-56 184-184-184-184 57-56 240 240-240 240zM80 760V600q0-83 58.5-141.5T280 400h247L383 256l57-56 240 240-240 240-57-56 144-144H280q-50 0-85 35t-35 85v160H80z"/>
</vector>
@@ -0,0 +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">
<path android:fillColor="#FF000000" android:pathData="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/>
</vector>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M501 460zm-21 420q-33 0-56.5-23.5T400 800h160q0 33-23.5 56.5T480 880zM160 760v-80h80V400q0-83 50-147.5T420 168v-28q0-25 17.5-42.5T480 80q25 0 42.5 17.5T540 140v28q29 7 53.5 20.5T639 220l-57 57q-21-17-47-27t-55-10q-66 0-113 47t-47 113v280h480v80H160zm496-219l-56-57q10-10 15-24t5-32q0-18-5-32.5T600 371l56-56q21 21 32.5 50t11.5 63q0 34-11.5 63T656 541zm43 43q29-29 45-70t16-86q0-45-16-86t-45-70l57-56q41 41 62.5 95.5T840 428q0 62-21.5 116.5T756 640l-57-56z"/>
</vector>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#ffffffff" android:pathData="M10 4h4c1.1 0 2 0.9 2 2v3l3 3v3h-6v7l-1 1-1-1v-7H5v-3l3-3V6c0-1.1 0.9-2 2-2z"/>
</vector>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#ffffffff" android:pathData="M10 6h4v3.83l3 3.34V13H7v0.17l3-3.34V6zm0-2C8.9 4 8 4.9 8 6v3l-3 3v3h6v7l1 1 1-1v-7h6v-3l-3-3V6c0-1.1-0.9-2-2-2h-4z"/>
</vector>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="#fff" android:pathData="M480 480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47zM160 800V688q0-34 17.5-62.5T224 582q62-31 126-46.5T480 520q66 0 130 15.5T736 582q29 15 46.5 43.5T800 688v112H160zm80-80h480v-32q0-11-5.5-20T700 654q-54-27-109-40.5T480 600q-56 0-111 13.5T260 654q-9 5-14.5 14t-5.5 20v32zm240-320q33 0 56.5-23.5T560 320q0-33-23.5-56.5T480 240q-33 0-56.5 23.5T400 320q0 33 23.5 56.5T480 400zm0-80zm0 400z"/>
</vector>
@@ -1,3 +0,0 @@
<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">
<path android:fillColor="#FF000000" android:pathData="M3 11h8V3H3v8zm2-6h4v4H5V5zM3 21h8v-8H3v8zm2-6h4v4H5v-4zm8-12v8h8V3h-8zm6 6h-4V5h4v4zm0 10h2v2h-2zm-6-6h2v2h-2zm2 2h2v2h-2zm-2 2h2v2h-2zm2 2h2v2h-2zm2-2h2v2h-2zm0-4h2v2h-2zm2 2h2v2h-2z"/>
</vector>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M80 880V160q0-33 23.5-56.5T160 80h640q33 0 56.5 23.5T880 160v480q0 33-23.5 56.5T800 720H240L80 880zm126-240h594V160H160v525l46-45zm-46 0V160v480z"/>
</vector>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M320 840q-33 0-56.5-23.5T240 760V200q0-33 23.5-56.5T320 120h320q33 0 56.5 23.5T720 200v560q0 33-23.5 56.5T640 840H320zm320-80V200H320v560h320zM508.5 308.5Q520 297 520 280t-11.5-28.5Q497 240 480 240t-28.5 11.5Q440 263 440 280t11.5 28.5Q463 320 480 320t28.5-11.5zM0 600V360h80v240H0zm120 80V280h80v400h-80zm760-80V360h80v240h-80zm-120 80V280h80v400h-80zm-440 80V200v560z"/>
</vector>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="32dp" android:height="32dp" android:viewportWidth="32" android:viewportHeight="32">
<path android:pathData="M0 0h6.737v1.684H1.684v1.684H0zm10.105 0h1.684v1.684h1.685v1.684h1.684v1.685h3.368V3.368h-1.684V1.684h-1.684V0h3.368v3.368h1.685v1.685H20.21v1.684h1.685v1.684h1.684V5.053h1.684V3.368h-1.684V1.684h-1.684V0h1.684v1.684h1.684V0h1.684v5.053h3.369v1.684h-5.053v1.684h1.684v1.684h-3.368v1.684h3.368v1.685h-3.368v3.368h3.368v1.684h3.369v-1.684h-3.369v-3.368H32v6.737h-3.368v1.684h-1.685v-1.684h-3.368v1.684h3.368v1.684h1.685v1.684h-1.685v1.684h3.369v1.685h-3.369v1.684h-1.684v-1.684h-1.684v-1.685h-1.684v-1.684h1.684v1.684h1.684v-3.368H15.158v1.684h5.053v1.684h-6.737v-1.684h-1.685v3.369h1.685v1.684H5.053v-1.684H1.684v-6.737h5.053v6.737h1.684v-3.369h1.684v-1.684H8.421v-1.684H6.737V20.21h1.684v-5.052h1.684v6.737h11.79v-11.79h-11.79v5.053H6.737v3.368H5.053v-1.684H3.368v3.369H1.684v1.684H0V20.21h1.684v-1.684H0v-3.368h5.053v-1.684h3.368v-3.369H3.368V6.737h1.685V5.053H3.368V3.368h3.369V1.684h1.684v1.684H6.737v3.369h1.684V5.053h1.684v3.368h1.684V6.737h1.685v1.684h3.368V6.737h-1.684V5.053h-1.684V3.368h-1.685v1.685h-1.684zm20.211 0H32v5.053h-1.684zM0 5.053h3.368v1.684H0zm28.632 1.684h1.684v5.052h-1.684zM0 8.421h1.684v1.684h1.684v3.369H1.684v-1.685H0zm11.789 3.368h8.422v1.685h-8.422zm0 1.685h1.685v5.052h5.052v-5.052h1.685v6.737h-8.422zm3.369 1.684h1.684v1.684h-1.684zM5.053 23.579H3.368v1.684h1.685zm25.263 0H32v1.684h-1.684zm-10.105 3.368h1.684v3.369h-1.684V32h-1.685v-3.368h1.685zm-5.053 1.685h1.684v1.684h-1.684zm15.158 0H32v1.684h-1.684zM1.684 30.316h1.684V32H1.684zm25.263 0h3.369V32h-3.369z" android:fillColor="#3e3e3e" android:fillType="evenOdd"/>
</vector>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="32dp" android:height="32dp" android:viewportWidth="32" android:viewportHeight="32">
<path android:pathData="M0 0h2v2h2V0h2v2H4v2H2v4h2V4h4v2h2V4h2V0h2v2h2V0h2v2h-2v4h6V2h-2V0h2v2h2V0h2v4h-2v2h2V4h2V0h2v2h2v2h-2v2h2v2h-2v2h2v2h-4v2h4v2h-2v2h-2v2h-2v-4h-2v2h-2v-2h2v-2h2V8h2V6h-2v2h-2v4h-2v2h-2v2h-2v-2h2v-2h2v-2h-4V8h-2V6h-2v2h2v2h2v4h-2v2h-2v2h-2v-2h-2v2h2v4h2v8h2v-2h4v2h2v-4h6v2h-2v2h2v-2h2v-2h2v2h-2v2h2v2H0zm4 28h2v-2h2v-2h2v-2H6v2H4v-2H2v4h2v2H2v2h2zm8-2h-2v2h2zm-6-6H4v-2h2v2h2v-6h4v2h2v-2h-2v-2h-2v-2H8V8H6v2H4v2H2v2h2v4H2v2h2v2h2zM8 0h2v2H8zm6 18h2v2h2v4h-2v-4h-2zm4 0h4v2h-4zm12 0h2v2h-2zm-8 2h2v2h4v2h-4v-2h-2zm8 2h2v2h-2zm-12 2h2v2h-2z" android:fillColor="#3e3e3e" android:fillType="evenOdd"/>
</vector>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="32dp" android:height="32dp" android:viewportWidth="32" android:viewportHeight="32">
<path android:pathData="M0 32h0.711V0H0zm1.422 0h0.711V0H1.422zm1.423 0h2.133V0H2.845zm2.844 0H6.4V0H5.689zm2.844 0h0.712V0H8.533zm1.423 0h0.711V0H9.956zm1.422 0h2.133V0h-2.133zm4.267 0h2.133V0h-2.133zm2.844 0h2.133V0h-2.133zm2.844 0h0.712V0h-0.712zm2.845 0h0.711V0h-0.711zm1.422 0h0.711V0H25.6zm2.845 0h2.133V0h-2.133zm2.844 0H32V0h-0.711z" android:fillColor="#3e3e3e" android:fillType="evenOdd"/>
</vector>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="32dp" android:height="32dp" android:viewportWidth="32" android:viewportHeight="32">
<path android:pathData="M0 2.909h20.363V0H0zm23.272 2.909h2.91V0h-2.91zm5.819-2.909H32V0h-2.909zM0 17.455h2.909V2.909H0zm17.454 0h2.909V2.909h-2.909zm-11.636-2.91h8.727V5.818H5.818zm17.454-5.818H32V5.818h-8.728zm0 5.818H32v-2.909h-8.728zm2.91 2.91H32v-2.91h-5.818zM0 20.364h20.363v-2.909H0zm23.272 0h2.91v-2.909h-2.91zm2.91 5.818H32v-5.818h-5.818zM0 26.182h5.818v-2.909H0zm11.636 0h8.727v-2.909h-8.727zm-8.727 2.909h2.909v-2.909H2.909zM20.363 32h2.909v-5.818h-2.909zm8.728-2.909H32v-2.909h-2.909zM0 32h14.545v-2.909H0zm26.182 0H32v-2.909h-5.818z" android:fillColor="#3e3e3e" android:fillType="evenOdd"/>
</vector>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="32dp" android:height="32dp" android:viewportWidth="32" android:viewportHeight="32">
<path android:pathData="M0 2h3v28H0zm4 0h1v28H4zm2 0h1v28H6zm2 0h1v28H8zm15 0h3v28h-3zm4 0h1v28h-1zm2 0h1v28h-1zm2 0h1v28h-1zM11 2h1v4h-1zm2 0h2v4h-2zm4 0h1v4h-1zm2 0h2v4h-2zm-8 6h2v4h-2zm4 0h1v4h-1zm2 0h3v4h-3zm-6 6h3v4h-3zm5 0h1v4h-1zm3 0h2v4h-2zm-8 6h1v4h-1zm3 0h2v4h-2zm4 0h3v4h-3zm-7 6h1v4h-1zm2 0h3v4h-3zm5 0h3v4h-3z" android:fillColor="#3e3e3e"/>
</vector>
@@ -0,0 +1,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="32dp" android:height="32dp" android:viewportWidth="32" android:viewportHeight="32">
<path android:pathData="M0 0v10.625h10.625V0H0zm8.125 8.125H2.5V2.5h5.625v5.625z" android:fillColor="#3e3e3e"/>
<path android:pathData="M4.063 4.063h2.5v2.5h-2.5zM21.375 0v10.625H32V0H21.375zM29.5 8.125h-5.625V2.5H29.5v5.625z" android:fillColor="#3e3e3e"/>
<path android:pathData="M25.438 4.063h2.5v2.5h-2.5zM0 21.375V32h10.625V21.375H0zM8.125 29.5H2.5v-5.625h5.625V29.5z" android:fillColor="#3e3e3e"/>
<path android:pathData="M4.063 25.438h2.5v2.5h-2.5zM2.5 12.313H5v2.5H2.5zm5 5v-2.5H5v2.5h2.438v2.5h2.5v-2.5H7.5zm10-12.5H20v2.5h-2.5zm-5-2.313H15v4.813h-2.5zM15 0h2.5v2.5H15z" android:fillColor="#3e3e3e"/>
<path android:pathData="M15 7.313v2.5h-2.5v2.5h5v-5H15zm2.5 14.875V19.75H15v-4.938h-2.5v5H15v2.438h2.5v2.438h5v-2.5h-5z" android:fillColor="#3e3e3e"/>
<path android:pathData="M17.5 12.313H20v5h-2.5zm12 2.437v-2.438h-4.563v2.5H22.5v2.5H25v2.438h7v-5h-2.5zm0 2.5H25v-2.438h4.5v2.438zm0 4.938H32v5h-2.5zM20 17.313h2.5v2.5H20zm2.5 7.375H25v2.5h-2.5zm2.5-2.5h2.5v2.5H25zm0 5V32h2.5v-2.313h2v-2.5H25zM12.5 22.25H15V27h-2.5zM20 29.5V27h-5v5h2.5v-2.5h2.438V32h2.5v-2.5H20zM7.5 12.313h5v2.5h-5zm-7.5 2.5h2.5v5H0z" android:fillColor="#3e3e3e"/>
</vector>

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